1152 lines
44 KiB
Python
1152 lines
44 KiB
Python
"""Functions for computing and verifying matchings in a graph."""
|
|
from collections import Counter
|
|
from itertools import combinations, repeat
|
|
|
|
import networkx as nx
|
|
from networkx.utils import not_implemented_for
|
|
|
|
__all__ = [
|
|
"is_matching",
|
|
"is_maximal_matching",
|
|
"is_perfect_matching",
|
|
"max_weight_matching",
|
|
"min_weight_matching",
|
|
"maximal_matching",
|
|
]
|
|
|
|
|
|
@not_implemented_for("multigraph")
|
|
@not_implemented_for("directed")
|
|
@nx._dispatch
|
|
def maximal_matching(G):
|
|
r"""Find a maximal matching in the graph.
|
|
|
|
A matching is a subset of edges in which no node occurs more than once.
|
|
A maximal matching cannot add more edges and still be a matching.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX graph
|
|
Undirected graph
|
|
|
|
Returns
|
|
-------
|
|
matching : set
|
|
A maximal matching of the graph.
|
|
|
|
Examples
|
|
--------
|
|
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
|
|
>>> sorted(nx.maximal_matching(G))
|
|
[(1, 2), (3, 5)]
|
|
|
|
Notes
|
|
-----
|
|
The algorithm greedily selects a maximal matching M of the graph G
|
|
(i.e. no superset of M exists). It runs in $O(|E|)$ time.
|
|
"""
|
|
matching = set()
|
|
nodes = set()
|
|
for edge in G.edges():
|
|
# If the edge isn't covered, add it to the matching
|
|
# then remove neighborhood of u and v from consideration.
|
|
u, v = edge
|
|
if u not in nodes and v not in nodes and u != v:
|
|
matching.add(edge)
|
|
nodes.update(edge)
|
|
return matching
|
|
|
|
|
|
def matching_dict_to_set(matching):
|
|
"""Converts matching dict format to matching set format
|
|
|
|
Converts a dictionary representing a matching (as returned by
|
|
:func:`max_weight_matching`) to a set representing a matching (as
|
|
returned by :func:`maximal_matching`).
|
|
|
|
In the definition of maximal matching adopted by NetworkX,
|
|
self-loops are not allowed, so the provided dictionary is expected
|
|
to never have any mapping from a key to itself. However, the
|
|
dictionary is expected to have mirrored key/value pairs, for
|
|
example, key ``u`` with value ``v`` and key ``v`` with value ``u``.
|
|
|
|
"""
|
|
edges = set()
|
|
for edge in matching.items():
|
|
u, v = edge
|
|
if (v, u) in edges or edge in edges:
|
|
continue
|
|
if u == v:
|
|
raise nx.NetworkXError(f"Selfloops cannot appear in matchings {edge}")
|
|
edges.add(edge)
|
|
return edges
|
|
|
|
|
|
@nx._dispatch
|
|
def is_matching(G, matching):
|
|
"""Return True if ``matching`` is a valid matching of ``G``
|
|
|
|
A *matching* in a graph is a set of edges in which no two distinct
|
|
edges share a common endpoint. Each node is incident to at most one
|
|
edge in the matching. The edges are said to be independent.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX graph
|
|
|
|
matching : dict or set
|
|
A dictionary or set representing a matching. If a dictionary, it
|
|
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
|
edge ``(u, v)`` in the matching. If a set, it must have elements
|
|
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
|
matching.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
Whether the given set or dictionary represents a valid matching
|
|
in the graph.
|
|
|
|
Raises
|
|
------
|
|
NetworkXError
|
|
If the proposed matching has an edge to a node not in G.
|
|
Or if the matching is not a collection of 2-tuple edges.
|
|
|
|
Examples
|
|
--------
|
|
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
|
|
>>> nx.is_maximal_matching(G, {1: 3, 2: 4}) # using dict to represent matching
|
|
True
|
|
|
|
>>> nx.is_matching(G, {(1, 3), (2, 4)}) # using set to represent matching
|
|
True
|
|
|
|
"""
|
|
if isinstance(matching, dict):
|
|
matching = matching_dict_to_set(matching)
|
|
|
|
nodes = set()
|
|
for edge in matching:
|
|
if len(edge) != 2:
|
|
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
|
u, v = edge
|
|
if u not in G or v not in G:
|
|
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
|
if u == v:
|
|
return False
|
|
if not G.has_edge(u, v):
|
|
return False
|
|
if u in nodes or v in nodes:
|
|
return False
|
|
nodes.update(edge)
|
|
return True
|
|
|
|
|
|
@nx._dispatch
|
|
def is_maximal_matching(G, matching):
|
|
"""Return True if ``matching`` is a maximal matching of ``G``
|
|
|
|
A *maximal matching* in a graph is a matching in which adding any
|
|
edge would cause the set to no longer be a valid matching.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX graph
|
|
|
|
matching : dict or set
|
|
A dictionary or set representing a matching. If a dictionary, it
|
|
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
|
edge ``(u, v)`` in the matching. If a set, it must have elements
|
|
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
|
matching.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
Whether the given set or dictionary represents a valid maximal
|
|
matching in the graph.
|
|
|
|
Examples
|
|
--------
|
|
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (3, 5)])
|
|
>>> nx.is_maximal_matching(G, {(1, 2), (3, 4)})
|
|
True
|
|
|
|
"""
|
|
if isinstance(matching, dict):
|
|
matching = matching_dict_to_set(matching)
|
|
# If the given set is not a matching, then it is not a maximal matching.
|
|
edges = set()
|
|
nodes = set()
|
|
for edge in matching:
|
|
if len(edge) != 2:
|
|
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
|
u, v = edge
|
|
if u not in G or v not in G:
|
|
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
|
if u == v:
|
|
return False
|
|
if not G.has_edge(u, v):
|
|
return False
|
|
if u in nodes or v in nodes:
|
|
return False
|
|
nodes.update(edge)
|
|
edges.add(edge)
|
|
edges.add((v, u))
|
|
# A matching is maximal if adding any new edge from G to it
|
|
# causes the resulting set to match some node twice.
|
|
# Be careful to check for adding selfloops
|
|
for u, v in G.edges:
|
|
if (u, v) not in edges:
|
|
# could add edge (u, v) to edges and have a bigger matching
|
|
if u not in nodes and v not in nodes and u != v:
|
|
return False
|
|
return True
|
|
|
|
|
|
@nx._dispatch
|
|
def is_perfect_matching(G, matching):
|
|
"""Return True if ``matching`` is a perfect matching for ``G``
|
|
|
|
A *perfect matching* in a graph is a matching in which exactly one edge
|
|
is incident upon each vertex.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX graph
|
|
|
|
matching : dict or set
|
|
A dictionary or set representing a matching. If a dictionary, it
|
|
must have ``matching[u] == v`` and ``matching[v] == u`` for each
|
|
edge ``(u, v)`` in the matching. If a set, it must have elements
|
|
of the form ``(u, v)``, where ``(u, v)`` is an edge in the
|
|
matching.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
Whether the given set or dictionary represents a valid perfect
|
|
matching in the graph.
|
|
|
|
Examples
|
|
--------
|
|
>>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6)])
|
|
>>> my_match = {1: 2, 3: 5, 4: 6}
|
|
>>> nx.is_perfect_matching(G, my_match)
|
|
True
|
|
|
|
"""
|
|
if isinstance(matching, dict):
|
|
matching = matching_dict_to_set(matching)
|
|
|
|
nodes = set()
|
|
for edge in matching:
|
|
if len(edge) != 2:
|
|
raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
|
|
u, v = edge
|
|
if u not in G or v not in G:
|
|
raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
|
|
if u == v:
|
|
return False
|
|
if not G.has_edge(u, v):
|
|
return False
|
|
if u in nodes or v in nodes:
|
|
return False
|
|
nodes.update(edge)
|
|
return len(nodes) == len(G)
|
|
|
|
|
|
@not_implemented_for("multigraph")
|
|
@not_implemented_for("directed")
|
|
@nx._dispatch(edge_attrs="weight")
|
|
def min_weight_matching(G, weight="weight"):
|
|
"""Computing a minimum-weight maximal matching of G.
|
|
|
|
Use the maximum-weight algorithm with edge weights subtracted
|
|
from the maximum weight of all edges.
|
|
|
|
A matching is a subset of edges in which no node occurs more than once.
|
|
The weight of a matching is the sum of the weights of its edges.
|
|
A maximal matching cannot add more edges and still be a matching.
|
|
The cardinality of a matching is the number of matched edges.
|
|
|
|
This method replaces the edge weights with 1 plus the maximum edge weight
|
|
minus the original edge weight.
|
|
|
|
new_weight = (max_weight + 1) - edge_weight
|
|
|
|
then runs :func:`max_weight_matching` with the new weights.
|
|
The max weight matching with these new weights corresponds
|
|
to the min weight matching using the original weights.
|
|
Adding 1 to the max edge weight keeps all edge weights positive
|
|
and as integers if they started as integers.
|
|
|
|
You might worry that adding 1 to each weight would make the algorithm
|
|
favor matchings with more edges. But we use the parameter
|
|
`maxcardinality=True` in `max_weight_matching` to ensure that the
|
|
number of edges in the competing matchings are the same and thus
|
|
the optimum does not change due to changes in the number of edges.
|
|
|
|
Read the documentation of `max_weight_matching` for more information.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX graph
|
|
Undirected graph
|
|
|
|
weight: string, optional (default='weight')
|
|
Edge data key corresponding to the edge weight.
|
|
If key not found, uses 1 as weight.
|
|
|
|
Returns
|
|
-------
|
|
matching : set
|
|
A minimal weight matching of the graph.
|
|
|
|
See Also
|
|
--------
|
|
max_weight_matching
|
|
"""
|
|
if len(G.edges) == 0:
|
|
return max_weight_matching(G, maxcardinality=True, weight=weight)
|
|
G_edges = G.edges(data=weight, default=1)
|
|
max_weight = 1 + max(w for _, _, w in G_edges)
|
|
InvG = nx.Graph()
|
|
edges = ((u, v, max_weight - w) for u, v, w in G_edges)
|
|
InvG.add_weighted_edges_from(edges, weight=weight)
|
|
return max_weight_matching(InvG, maxcardinality=True, weight=weight)
|
|
|
|
|
|
@not_implemented_for("multigraph")
|
|
@not_implemented_for("directed")
|
|
@nx._dispatch(edge_attrs="weight")
|
|
def max_weight_matching(G, maxcardinality=False, weight="weight"):
|
|
"""Compute a maximum-weighted matching of G.
|
|
|
|
A matching is a subset of edges in which no node occurs more than once.
|
|
The weight of a matching is the sum of the weights of its edges.
|
|
A maximal matching cannot add more edges and still be a matching.
|
|
The cardinality of a matching is the number of matched edges.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX graph
|
|
Undirected graph
|
|
|
|
maxcardinality: bool, optional (default=False)
|
|
If maxcardinality is True, compute the maximum-cardinality matching
|
|
with maximum weight among all maximum-cardinality matchings.
|
|
|
|
weight: string, optional (default='weight')
|
|
Edge data key corresponding to the edge weight.
|
|
If key not found, uses 1 as weight.
|
|
|
|
|
|
Returns
|
|
-------
|
|
matching : set
|
|
A maximal matching of the graph.
|
|
|
|
Examples
|
|
--------
|
|
>>> G = nx.Graph()
|
|
>>> edges = [(1, 2, 6), (1, 3, 2), (2, 3, 1), (2, 4, 7), (3, 5, 9), (4, 5, 3)]
|
|
>>> G.add_weighted_edges_from(edges)
|
|
>>> sorted(nx.max_weight_matching(G))
|
|
[(2, 4), (5, 3)]
|
|
|
|
Notes
|
|
-----
|
|
If G has edges with weight attributes the edge data are used as
|
|
weight values else the weights are assumed to be 1.
|
|
|
|
This function takes time O(number_of_nodes ** 3).
|
|
|
|
If all edge weights are integers, the algorithm uses only integer
|
|
computations. If floating point weights are used, the algorithm
|
|
could return a slightly suboptimal matching due to numeric
|
|
precision errors.
|
|
|
|
This method is based on the "blossom" method for finding augmenting
|
|
paths and the "primal-dual" method for finding a matching of maximum
|
|
weight, both methods invented by Jack Edmonds [1]_.
|
|
|
|
Bipartite graphs can also be matched using the functions present in
|
|
:mod:`networkx.algorithms.bipartite.matching`.
|
|
|
|
References
|
|
----------
|
|
.. [1] "Efficient Algorithms for Finding Maximum Matching in Graphs",
|
|
Zvi Galil, ACM Computing Surveys, 1986.
|
|
"""
|
|
#
|
|
# The algorithm is taken from "Efficient Algorithms for Finding Maximum
|
|
# Matching in Graphs" by Zvi Galil, ACM Computing Surveys, 1986.
|
|
# It is based on the "blossom" method for finding augmenting paths and
|
|
# the "primal-dual" method for finding a matching of maximum weight, both
|
|
# methods invented by Jack Edmonds.
|
|
#
|
|
# A C program for maximum weight matching by Ed Rothberg was used
|
|
# extensively to validate this new code.
|
|
#
|
|
# Many terms used in the code comments are explained in the paper
|
|
# by Galil. You will probably need the paper to make sense of this code.
|
|
#
|
|
|
|
class NoNode:
|
|
"""Dummy value which is different from any node."""
|
|
|
|
class Blossom:
|
|
"""Representation of a non-trivial blossom or sub-blossom."""
|
|
|
|
__slots__ = ["childs", "edges", "mybestedges"]
|
|
|
|
# b.childs is an ordered list of b's sub-blossoms, starting with
|
|
# the base and going round the blossom.
|
|
|
|
# b.edges is the list of b's connecting edges, such that
|
|
# b.edges[i] = (v, w) where v is a vertex in b.childs[i]
|
|
# and w is a vertex in b.childs[wrap(i+1)].
|
|
|
|
# If b is a top-level S-blossom,
|
|
# b.mybestedges is a list of least-slack edges to neighbouring
|
|
# S-blossoms, or None if no such list has been computed yet.
|
|
# This is used for efficient computation of delta3.
|
|
|
|
# Generate the blossom's leaf vertices.
|
|
def leaves(self):
|
|
stack = [*self.childs]
|
|
while stack:
|
|
t = stack.pop()
|
|
if isinstance(t, Blossom):
|
|
stack.extend(t.childs)
|
|
else:
|
|
yield t
|
|
|
|
# Get a list of vertices.
|
|
gnodes = list(G)
|
|
if not gnodes:
|
|
return set() # don't bother with empty graphs
|
|
|
|
# Find the maximum edge weight.
|
|
maxweight = 0
|
|
allinteger = True
|
|
for i, j, d in G.edges(data=True):
|
|
wt = d.get(weight, 1)
|
|
if i != j and wt > maxweight:
|
|
maxweight = wt
|
|
allinteger = allinteger and (str(type(wt)).split("'")[1] in ("int", "long"))
|
|
|
|
# If v is a matched vertex, mate[v] is its partner vertex.
|
|
# If v is a single vertex, v does not occur as a key in mate.
|
|
# Initially all vertices are single; updated during augmentation.
|
|
mate = {}
|
|
|
|
# If b is a top-level blossom,
|
|
# label.get(b) is None if b is unlabeled (free),
|
|
# 1 if b is an S-blossom,
|
|
# 2 if b is a T-blossom.
|
|
# The label of a vertex is found by looking at the label of its top-level
|
|
# containing blossom.
|
|
# If v is a vertex inside a T-blossom, label[v] is 2 iff v is reachable
|
|
# from an S-vertex outside the blossom.
|
|
# Labels are assigned during a stage and reset after each augmentation.
|
|
label = {}
|
|
|
|
# If b is a labeled top-level blossom,
|
|
# labeledge[b] = (v, w) is the edge through which b obtained its label
|
|
# such that w is a vertex in b, or None if b's base vertex is single.
|
|
# If w is a vertex inside a T-blossom and label[w] == 2,
|
|
# labeledge[w] = (v, w) is an edge through which w is reachable from
|
|
# outside the blossom.
|
|
labeledge = {}
|
|
|
|
# If v is a vertex, inblossom[v] is the top-level blossom to which v
|
|
# belongs.
|
|
# If v is a top-level vertex, inblossom[v] == v since v is itself
|
|
# a (trivial) top-level blossom.
|
|
# Initially all vertices are top-level trivial blossoms.
|
|
inblossom = dict(zip(gnodes, gnodes))
|
|
|
|
# If b is a sub-blossom,
|
|
# blossomparent[b] is its immediate parent (sub-)blossom.
|
|
# If b is a top-level blossom, blossomparent[b] is None.
|
|
blossomparent = dict(zip(gnodes, repeat(None)))
|
|
|
|
# If b is a (sub-)blossom,
|
|
# blossombase[b] is its base VERTEX (i.e. recursive sub-blossom).
|
|
blossombase = dict(zip(gnodes, gnodes))
|
|
|
|
# If w is a free vertex (or an unreached vertex inside a T-blossom),
|
|
# bestedge[w] = (v, w) is the least-slack edge from an S-vertex,
|
|
# or None if there is no such edge.
|
|
# If b is a (possibly trivial) top-level S-blossom,
|
|
# bestedge[b] = (v, w) is the least-slack edge to a different S-blossom
|
|
# (v inside b), or None if there is no such edge.
|
|
# This is used for efficient computation of delta2 and delta3.
|
|
bestedge = {}
|
|
|
|
# If v is a vertex,
|
|
# dualvar[v] = 2 * u(v) where u(v) is the v's variable in the dual
|
|
# optimization problem (if all edge weights are integers, multiplication
|
|
# by two ensures that all values remain integers throughout the algorithm).
|
|
# Initially, u(v) = maxweight / 2.
|
|
dualvar = dict(zip(gnodes, repeat(maxweight)))
|
|
|
|
# If b is a non-trivial blossom,
|
|
# blossomdual[b] = z(b) where z(b) is b's variable in the dual
|
|
# optimization problem.
|
|
blossomdual = {}
|
|
|
|
# If (v, w) in allowedge or (w, v) in allowedg, then the edge
|
|
# (v, w) is known to have zero slack in the optimization problem;
|
|
# otherwise the edge may or may not have zero slack.
|
|
allowedge = {}
|
|
|
|
# Queue of newly discovered S-vertices.
|
|
queue = []
|
|
|
|
# Return 2 * slack of edge (v, w) (does not work inside blossoms).
|
|
def slack(v, w):
|
|
return dualvar[v] + dualvar[w] - 2 * G[v][w].get(weight, 1)
|
|
|
|
# Assign label t to the top-level blossom containing vertex w,
|
|
# coming through an edge from vertex v.
|
|
def assignLabel(w, t, v):
|
|
b = inblossom[w]
|
|
assert label.get(w) is None and label.get(b) is None
|
|
label[w] = label[b] = t
|
|
if v is not None:
|
|
labeledge[w] = labeledge[b] = (v, w)
|
|
else:
|
|
labeledge[w] = labeledge[b] = None
|
|
bestedge[w] = bestedge[b] = None
|
|
if t == 1:
|
|
# b became an S-vertex/blossom; add it(s vertices) to the queue.
|
|
if isinstance(b, Blossom):
|
|
queue.extend(b.leaves())
|
|
else:
|
|
queue.append(b)
|
|
elif t == 2:
|
|
# b became a T-vertex/blossom; assign label S to its mate.
|
|
# (If b is a non-trivial blossom, its base is the only vertex
|
|
# with an external mate.)
|
|
base = blossombase[b]
|
|
assignLabel(mate[base], 1, base)
|
|
|
|
# Trace back from vertices v and w to discover either a new blossom
|
|
# or an augmenting path. Return the base vertex of the new blossom,
|
|
# or NoNode if an augmenting path was found.
|
|
def scanBlossom(v, w):
|
|
# Trace back from v and w, placing breadcrumbs as we go.
|
|
path = []
|
|
base = NoNode
|
|
while v is not NoNode:
|
|
# Look for a breadcrumb in v's blossom or put a new breadcrumb.
|
|
b = inblossom[v]
|
|
if label[b] & 4:
|
|
base = blossombase[b]
|
|
break
|
|
assert label[b] == 1
|
|
path.append(b)
|
|
label[b] = 5
|
|
# Trace one step back.
|
|
if labeledge[b] is None:
|
|
# The base of blossom b is single; stop tracing this path.
|
|
assert blossombase[b] not in mate
|
|
v = NoNode
|
|
else:
|
|
assert labeledge[b][0] == mate[blossombase[b]]
|
|
v = labeledge[b][0]
|
|
b = inblossom[v]
|
|
assert label[b] == 2
|
|
# b is a T-blossom; trace one more step back.
|
|
v = labeledge[b][0]
|
|
# Swap v and w so that we alternate between both paths.
|
|
if w is not NoNode:
|
|
v, w = w, v
|
|
# Remove breadcrumbs.
|
|
for b in path:
|
|
label[b] = 1
|
|
# Return base vertex, if we found one.
|
|
return base
|
|
|
|
# Construct a new blossom with given base, through S-vertices v and w.
|
|
# Label the new blossom as S; set its dual variable to zero;
|
|
# relabel its T-vertices to S and add them to the queue.
|
|
def addBlossom(base, v, w):
|
|
bb = inblossom[base]
|
|
bv = inblossom[v]
|
|
bw = inblossom[w]
|
|
# Create blossom.
|
|
b = Blossom()
|
|
blossombase[b] = base
|
|
blossomparent[b] = None
|
|
blossomparent[bb] = b
|
|
# Make list of sub-blossoms and their interconnecting edge endpoints.
|
|
b.childs = path = []
|
|
b.edges = edgs = [(v, w)]
|
|
# Trace back from v to base.
|
|
while bv != bb:
|
|
# Add bv to the new blossom.
|
|
blossomparent[bv] = b
|
|
path.append(bv)
|
|
edgs.append(labeledge[bv])
|
|
assert label[bv] == 2 or (
|
|
label[bv] == 1 and labeledge[bv][0] == mate[blossombase[bv]]
|
|
)
|
|
# Trace one step back.
|
|
v = labeledge[bv][0]
|
|
bv = inblossom[v]
|
|
# Add base sub-blossom; reverse lists.
|
|
path.append(bb)
|
|
path.reverse()
|
|
edgs.reverse()
|
|
# Trace back from w to base.
|
|
while bw != bb:
|
|
# Add bw to the new blossom.
|
|
blossomparent[bw] = b
|
|
path.append(bw)
|
|
edgs.append((labeledge[bw][1], labeledge[bw][0]))
|
|
assert label[bw] == 2 or (
|
|
label[bw] == 1 and labeledge[bw][0] == mate[blossombase[bw]]
|
|
)
|
|
# Trace one step back.
|
|
w = labeledge[bw][0]
|
|
bw = inblossom[w]
|
|
# Set label to S.
|
|
assert label[bb] == 1
|
|
label[b] = 1
|
|
labeledge[b] = labeledge[bb]
|
|
# Set dual variable to zero.
|
|
blossomdual[b] = 0
|
|
# Relabel vertices.
|
|
for v in b.leaves():
|
|
if label[inblossom[v]] == 2:
|
|
# This T-vertex now turns into an S-vertex because it becomes
|
|
# part of an S-blossom; add it to the queue.
|
|
queue.append(v)
|
|
inblossom[v] = b
|
|
# Compute b.mybestedges.
|
|
bestedgeto = {}
|
|
for bv in path:
|
|
if isinstance(bv, Blossom):
|
|
if bv.mybestedges is not None:
|
|
# Walk this subblossom's least-slack edges.
|
|
nblist = bv.mybestedges
|
|
# The sub-blossom won't need this data again.
|
|
bv.mybestedges = None
|
|
else:
|
|
# This subblossom does not have a list of least-slack
|
|
# edges; get the information from the vertices.
|
|
nblist = [
|
|
(v, w) for v in bv.leaves() for w in G.neighbors(v) if v != w
|
|
]
|
|
else:
|
|
nblist = [(bv, w) for w in G.neighbors(bv) if bv != w]
|
|
for k in nblist:
|
|
(i, j) = k
|
|
if inblossom[j] == b:
|
|
i, j = j, i
|
|
bj = inblossom[j]
|
|
if (
|
|
bj != b
|
|
and label.get(bj) == 1
|
|
and ((bj not in bestedgeto) or slack(i, j) < slack(*bestedgeto[bj]))
|
|
):
|
|
bestedgeto[bj] = k
|
|
# Forget about least-slack edge of the subblossom.
|
|
bestedge[bv] = None
|
|
b.mybestedges = list(bestedgeto.values())
|
|
# Select bestedge[b].
|
|
mybestedge = None
|
|
bestedge[b] = None
|
|
for k in b.mybestedges:
|
|
kslack = slack(*k)
|
|
if mybestedge is None or kslack < mybestslack:
|
|
mybestedge = k
|
|
mybestslack = kslack
|
|
bestedge[b] = mybestedge
|
|
|
|
# Expand the given top-level blossom.
|
|
def expandBlossom(b, endstage):
|
|
# This is an obnoxiously complicated recursive function for the sake of
|
|
# a stack-transformation. So, we hack around the complexity by using
|
|
# a trampoline pattern. By yielding the arguments to each recursive
|
|
# call, we keep the actual callstack flat.
|
|
|
|
def _recurse(b, endstage):
|
|
# Convert sub-blossoms into top-level blossoms.
|
|
for s in b.childs:
|
|
blossomparent[s] = None
|
|
if isinstance(s, Blossom):
|
|
if endstage and blossomdual[s] == 0:
|
|
# Recursively expand this sub-blossom.
|
|
yield s
|
|
else:
|
|
for v in s.leaves():
|
|
inblossom[v] = s
|
|
else:
|
|
inblossom[s] = s
|
|
# If we expand a T-blossom during a stage, its sub-blossoms must be
|
|
# relabeled.
|
|
if (not endstage) and label.get(b) == 2:
|
|
# Start at the sub-blossom through which the expanding
|
|
# blossom obtained its label, and relabel sub-blossoms untili
|
|
# we reach the base.
|
|
# Figure out through which sub-blossom the expanding blossom
|
|
# obtained its label initially.
|
|
entrychild = inblossom[labeledge[b][1]]
|
|
# Decide in which direction we will go round the blossom.
|
|
j = b.childs.index(entrychild)
|
|
if j & 1:
|
|
# Start index is odd; go forward and wrap.
|
|
j -= len(b.childs)
|
|
jstep = 1
|
|
else:
|
|
# Start index is even; go backward.
|
|
jstep = -1
|
|
# Move along the blossom until we get to the base.
|
|
v, w = labeledge[b]
|
|
while j != 0:
|
|
# Relabel the T-sub-blossom.
|
|
if jstep == 1:
|
|
p, q = b.edges[j]
|
|
else:
|
|
q, p = b.edges[j - 1]
|
|
label[w] = None
|
|
label[q] = None
|
|
assignLabel(w, 2, v)
|
|
# Step to the next S-sub-blossom and note its forward edge.
|
|
allowedge[(p, q)] = allowedge[(q, p)] = True
|
|
j += jstep
|
|
if jstep == 1:
|
|
v, w = b.edges[j]
|
|
else:
|
|
w, v = b.edges[j - 1]
|
|
# Step to the next T-sub-blossom.
|
|
allowedge[(v, w)] = allowedge[(w, v)] = True
|
|
j += jstep
|
|
# Relabel the base T-sub-blossom WITHOUT stepping through to
|
|
# its mate (so don't call assignLabel).
|
|
bw = b.childs[j]
|
|
label[w] = label[bw] = 2
|
|
labeledge[w] = labeledge[bw] = (v, w)
|
|
bestedge[bw] = None
|
|
# Continue along the blossom until we get back to entrychild.
|
|
j += jstep
|
|
while b.childs[j] != entrychild:
|
|
# Examine the vertices of the sub-blossom to see whether
|
|
# it is reachable from a neighbouring S-vertex outside the
|
|
# expanding blossom.
|
|
bv = b.childs[j]
|
|
if label.get(bv) == 1:
|
|
# This sub-blossom just got label S through one of its
|
|
# neighbours; leave it be.
|
|
j += jstep
|
|
continue
|
|
if isinstance(bv, Blossom):
|
|
for v in bv.leaves():
|
|
if label.get(v):
|
|
break
|
|
else:
|
|
v = bv
|
|
# If the sub-blossom contains a reachable vertex, assign
|
|
# label T to the sub-blossom.
|
|
if label.get(v):
|
|
assert label[v] == 2
|
|
assert inblossom[v] == bv
|
|
label[v] = None
|
|
label[mate[blossombase[bv]]] = None
|
|
assignLabel(v, 2, labeledge[v][0])
|
|
j += jstep
|
|
# Remove the expanded blossom entirely.
|
|
label.pop(b, None)
|
|
labeledge.pop(b, None)
|
|
bestedge.pop(b, None)
|
|
del blossomparent[b]
|
|
del blossombase[b]
|
|
del blossomdual[b]
|
|
|
|
# Now, we apply the trampoline pattern. We simulate a recursive
|
|
# callstack by maintaining a stack of generators, each yielding a
|
|
# sequence of function arguments. We grow the stack by appending a call
|
|
# to _recurse on each argument tuple, and shrink the stack whenever a
|
|
# generator is exhausted.
|
|
stack = [_recurse(b, endstage)]
|
|
while stack:
|
|
top = stack[-1]
|
|
for s in top:
|
|
stack.append(_recurse(s, endstage))
|
|
break
|
|
else:
|
|
stack.pop()
|
|
|
|
# Swap matched/unmatched edges over an alternating path through blossom b
|
|
# between vertex v and the base vertex. Keep blossom bookkeeping
|
|
# consistent.
|
|
def augmentBlossom(b, v):
|
|
# This is an obnoxiously complicated recursive function for the sake of
|
|
# a stack-transformation. So, we hack around the complexity by using
|
|
# a trampoline pattern. By yielding the arguments to each recursive
|
|
# call, we keep the actual callstack flat.
|
|
|
|
def _recurse(b, v):
|
|
# Bubble up through the blossom tree from vertex v to an immediate
|
|
# sub-blossom of b.
|
|
t = v
|
|
while blossomparent[t] != b:
|
|
t = blossomparent[t]
|
|
# Recursively deal with the first sub-blossom.
|
|
if isinstance(t, Blossom):
|
|
yield (t, v)
|
|
# Decide in which direction we will go round the blossom.
|
|
i = j = b.childs.index(t)
|
|
if i & 1:
|
|
# Start index is odd; go forward and wrap.
|
|
j -= len(b.childs)
|
|
jstep = 1
|
|
else:
|
|
# Start index is even; go backward.
|
|
jstep = -1
|
|
# Move along the blossom until we get to the base.
|
|
while j != 0:
|
|
# Step to the next sub-blossom and augment it recursively.
|
|
j += jstep
|
|
t = b.childs[j]
|
|
if jstep == 1:
|
|
w, x = b.edges[j]
|
|
else:
|
|
x, w = b.edges[j - 1]
|
|
if isinstance(t, Blossom):
|
|
yield (t, w)
|
|
# Step to the next sub-blossom and augment it recursively.
|
|
j += jstep
|
|
t = b.childs[j]
|
|
if isinstance(t, Blossom):
|
|
yield (t, x)
|
|
# Match the edge connecting those sub-blossoms.
|
|
mate[w] = x
|
|
mate[x] = w
|
|
# Rotate the list of sub-blossoms to put the new base at the front.
|
|
b.childs = b.childs[i:] + b.childs[:i]
|
|
b.edges = b.edges[i:] + b.edges[:i]
|
|
blossombase[b] = blossombase[b.childs[0]]
|
|
assert blossombase[b] == v
|
|
|
|
# Now, we apply the trampoline pattern. We simulate a recursive
|
|
# callstack by maintaining a stack of generators, each yielding a
|
|
# sequence of function arguments. We grow the stack by appending a call
|
|
# to _recurse on each argument tuple, and shrink the stack whenever a
|
|
# generator is exhausted.
|
|
stack = [_recurse(b, v)]
|
|
while stack:
|
|
top = stack[-1]
|
|
for args in top:
|
|
stack.append(_recurse(*args))
|
|
break
|
|
else:
|
|
stack.pop()
|
|
|
|
# Swap matched/unmatched edges over an alternating path between two
|
|
# single vertices. The augmenting path runs through S-vertices v and w.
|
|
def augmentMatching(v, w):
|
|
for s, j in ((v, w), (w, v)):
|
|
# Match vertex s to vertex j. Then trace back from s
|
|
# until we find a single vertex, swapping matched and unmatched
|
|
# edges as we go.
|
|
while 1:
|
|
bs = inblossom[s]
|
|
assert label[bs] == 1
|
|
assert (labeledge[bs] is None and blossombase[bs] not in mate) or (
|
|
labeledge[bs][0] == mate[blossombase[bs]]
|
|
)
|
|
# Augment through the S-blossom from s to base.
|
|
if isinstance(bs, Blossom):
|
|
augmentBlossom(bs, s)
|
|
# Update mate[s]
|
|
mate[s] = j
|
|
# Trace one step back.
|
|
if labeledge[bs] is None:
|
|
# Reached single vertex; stop.
|
|
break
|
|
t = labeledge[bs][0]
|
|
bt = inblossom[t]
|
|
assert label[bt] == 2
|
|
# Trace one more step back.
|
|
s, j = labeledge[bt]
|
|
# Augment through the T-blossom from j to base.
|
|
assert blossombase[bt] == t
|
|
if isinstance(bt, Blossom):
|
|
augmentBlossom(bt, j)
|
|
# Update mate[j]
|
|
mate[j] = s
|
|
|
|
# Verify that the optimum solution has been reached.
|
|
def verifyOptimum():
|
|
if maxcardinality:
|
|
# Vertices may have negative dual;
|
|
# find a constant non-negative number to add to all vertex duals.
|
|
vdualoffset = max(0, -min(dualvar.values()))
|
|
else:
|
|
vdualoffset = 0
|
|
# 0. all dual variables are non-negative
|
|
assert min(dualvar.values()) + vdualoffset >= 0
|
|
assert len(blossomdual) == 0 or min(blossomdual.values()) >= 0
|
|
# 0. all edges have non-negative slack and
|
|
# 1. all matched edges have zero slack;
|
|
for i, j, d in G.edges(data=True):
|
|
wt = d.get(weight, 1)
|
|
if i == j:
|
|
continue # ignore self-loops
|
|
s = dualvar[i] + dualvar[j] - 2 * wt
|
|
iblossoms = [i]
|
|
jblossoms = [j]
|
|
while blossomparent[iblossoms[-1]] is not None:
|
|
iblossoms.append(blossomparent[iblossoms[-1]])
|
|
while blossomparent[jblossoms[-1]] is not None:
|
|
jblossoms.append(blossomparent[jblossoms[-1]])
|
|
iblossoms.reverse()
|
|
jblossoms.reverse()
|
|
for bi, bj in zip(iblossoms, jblossoms):
|
|
if bi != bj:
|
|
break
|
|
s += 2 * blossomdual[bi]
|
|
assert s >= 0
|
|
if mate.get(i) == j or mate.get(j) == i:
|
|
assert mate[i] == j and mate[j] == i
|
|
assert s == 0
|
|
# 2. all single vertices have zero dual value;
|
|
for v in gnodes:
|
|
assert (v in mate) or dualvar[v] + vdualoffset == 0
|
|
# 3. all blossoms with positive dual value are full.
|
|
for b in blossomdual:
|
|
if blossomdual[b] > 0:
|
|
assert len(b.edges) % 2 == 1
|
|
for i, j in b.edges[1::2]:
|
|
assert mate[i] == j and mate[j] == i
|
|
# Ok.
|
|
|
|
# Main loop: continue until no further improvement is possible.
|
|
while 1:
|
|
# Each iteration of this loop is a "stage".
|
|
# A stage finds an augmenting path and uses that to improve
|
|
# the matching.
|
|
|
|
# Remove labels from top-level blossoms/vertices.
|
|
label.clear()
|
|
labeledge.clear()
|
|
|
|
# Forget all about least-slack edges.
|
|
bestedge.clear()
|
|
for b in blossomdual:
|
|
b.mybestedges = None
|
|
|
|
# Loss of labeling means that we can not be sure that currently
|
|
# allowable edges remain allowable throughout this stage.
|
|
allowedge.clear()
|
|
|
|
# Make queue empty.
|
|
queue[:] = []
|
|
|
|
# Label single blossoms/vertices with S and put them in the queue.
|
|
for v in gnodes:
|
|
if (v not in mate) and label.get(inblossom[v]) is None:
|
|
assignLabel(v, 1, None)
|
|
|
|
# Loop until we succeed in augmenting the matching.
|
|
augmented = 0
|
|
while 1:
|
|
# Each iteration of this loop is a "substage".
|
|
# A substage tries to find an augmenting path;
|
|
# if found, the path is used to improve the matching and
|
|
# the stage ends. If there is no augmenting path, the
|
|
# primal-dual method is used to pump some slack out of
|
|
# the dual variables.
|
|
|
|
# Continue labeling until all vertices which are reachable
|
|
# through an alternating path have got a label.
|
|
while queue and not augmented:
|
|
# Take an S vertex from the queue.
|
|
v = queue.pop()
|
|
assert label[inblossom[v]] == 1
|
|
|
|
# Scan its neighbours:
|
|
for w in G.neighbors(v):
|
|
if w == v:
|
|
continue # ignore self-loops
|
|
# w is a neighbour to v
|
|
bv = inblossom[v]
|
|
bw = inblossom[w]
|
|
if bv == bw:
|
|
# this edge is internal to a blossom; ignore it
|
|
continue
|
|
if (v, w) not in allowedge:
|
|
kslack = slack(v, w)
|
|
if kslack <= 0:
|
|
# edge k has zero slack => it is allowable
|
|
allowedge[(v, w)] = allowedge[(w, v)] = True
|
|
if (v, w) in allowedge:
|
|
if label.get(bw) is None:
|
|
# (C1) w is a free vertex;
|
|
# label w with T and label its mate with S (R12).
|
|
assignLabel(w, 2, v)
|
|
elif label.get(bw) == 1:
|
|
# (C2) w is an S-vertex (not in the same blossom);
|
|
# follow back-links to discover either an
|
|
# augmenting path or a new blossom.
|
|
base = scanBlossom(v, w)
|
|
if base is not NoNode:
|
|
# Found a new blossom; add it to the blossom
|
|
# bookkeeping and turn it into an S-blossom.
|
|
addBlossom(base, v, w)
|
|
else:
|
|
# Found an augmenting path; augment the
|
|
# matching and end this stage.
|
|
augmentMatching(v, w)
|
|
augmented = 1
|
|
break
|
|
elif label.get(w) is None:
|
|
# w is inside a T-blossom, but w itself has not
|
|
# yet been reached from outside the blossom;
|
|
# mark it as reached (we need this to relabel
|
|
# during T-blossom expansion).
|
|
assert label[bw] == 2
|
|
label[w] = 2
|
|
labeledge[w] = (v, w)
|
|
elif label.get(bw) == 1:
|
|
# keep track of the least-slack non-allowable edge to
|
|
# a different S-blossom.
|
|
if bestedge.get(bv) is None or kslack < slack(*bestedge[bv]):
|
|
bestedge[bv] = (v, w)
|
|
elif label.get(w) is None:
|
|
# w is a free vertex (or an unreached vertex inside
|
|
# a T-blossom) but we can not reach it yet;
|
|
# keep track of the least-slack edge that reaches w.
|
|
if bestedge.get(w) is None or kslack < slack(*bestedge[w]):
|
|
bestedge[w] = (v, w)
|
|
|
|
if augmented:
|
|
break
|
|
|
|
# There is no augmenting path under these constraints;
|
|
# compute delta and reduce slack in the optimization problem.
|
|
# (Note that our vertex dual variables, edge slacks and delta's
|
|
# are pre-multiplied by two.)
|
|
deltatype = -1
|
|
delta = deltaedge = deltablossom = None
|
|
|
|
# Compute delta1: the minimum value of any vertex dual.
|
|
if not maxcardinality:
|
|
deltatype = 1
|
|
delta = min(dualvar.values())
|
|
|
|
# Compute delta2: the minimum slack on any edge between
|
|
# an S-vertex and a free vertex.
|
|
for v in G.nodes():
|
|
if label.get(inblossom[v]) is None and bestedge.get(v) is not None:
|
|
d = slack(*bestedge[v])
|
|
if deltatype == -1 or d < delta:
|
|
delta = d
|
|
deltatype = 2
|
|
deltaedge = bestedge[v]
|
|
|
|
# Compute delta3: half the minimum slack on any edge between
|
|
# a pair of S-blossoms.
|
|
for b in blossomparent:
|
|
if (
|
|
blossomparent[b] is None
|
|
and label.get(b) == 1
|
|
and bestedge.get(b) is not None
|
|
):
|
|
kslack = slack(*bestedge[b])
|
|
if allinteger:
|
|
assert (kslack % 2) == 0
|
|
d = kslack // 2
|
|
else:
|
|
d = kslack / 2.0
|
|
if deltatype == -1 or d < delta:
|
|
delta = d
|
|
deltatype = 3
|
|
deltaedge = bestedge[b]
|
|
|
|
# Compute delta4: minimum z variable of any T-blossom.
|
|
for b in blossomdual:
|
|
if (
|
|
blossomparent[b] is None
|
|
and label.get(b) == 2
|
|
and (deltatype == -1 or blossomdual[b] < delta)
|
|
):
|
|
delta = blossomdual[b]
|
|
deltatype = 4
|
|
deltablossom = b
|
|
|
|
if deltatype == -1:
|
|
# No further improvement possible; max-cardinality optimum
|
|
# reached. Do a final delta update to make the optimum
|
|
# verifiable.
|
|
assert maxcardinality
|
|
deltatype = 1
|
|
delta = max(0, min(dualvar.values()))
|
|
|
|
# Update dual variables according to delta.
|
|
for v in gnodes:
|
|
if label.get(inblossom[v]) == 1:
|
|
# S-vertex: 2*u = 2*u - 2*delta
|
|
dualvar[v] -= delta
|
|
elif label.get(inblossom[v]) == 2:
|
|
# T-vertex: 2*u = 2*u + 2*delta
|
|
dualvar[v] += delta
|
|
for b in blossomdual:
|
|
if blossomparent[b] is None:
|
|
if label.get(b) == 1:
|
|
# top-level S-blossom: z = z + 2*delta
|
|
blossomdual[b] += delta
|
|
elif label.get(b) == 2:
|
|
# top-level T-blossom: z = z - 2*delta
|
|
blossomdual[b] -= delta
|
|
|
|
# Take action at the point where minimum delta occurred.
|
|
if deltatype == 1:
|
|
# No further improvement possible; optimum reached.
|
|
break
|
|
elif deltatype == 2:
|
|
# Use the least-slack edge to continue the search.
|
|
(v, w) = deltaedge
|
|
assert label[inblossom[v]] == 1
|
|
allowedge[(v, w)] = allowedge[(w, v)] = True
|
|
queue.append(v)
|
|
elif deltatype == 3:
|
|
# Use the least-slack edge to continue the search.
|
|
(v, w) = deltaedge
|
|
allowedge[(v, w)] = allowedge[(w, v)] = True
|
|
assert label[inblossom[v]] == 1
|
|
queue.append(v)
|
|
elif deltatype == 4:
|
|
# Expand the least-z blossom.
|
|
expandBlossom(deltablossom, False)
|
|
|
|
# End of a this substage.
|
|
|
|
# Paranoia check that the matching is symmetric.
|
|
for v in mate:
|
|
assert mate[mate[v]] == v
|
|
|
|
# Stop when no more augmenting path can be found.
|
|
if not augmented:
|
|
break
|
|
|
|
# End of a stage; expand all S-blossoms which have zero dual.
|
|
for b in list(blossomdual.keys()):
|
|
if b not in blossomdual:
|
|
continue # already expanded
|
|
if blossomparent[b] is None and label.get(b) == 1 and blossomdual[b] == 0:
|
|
expandBlossom(b, True)
|
|
|
|
# Verify that we reached the optimum solution (only for integer weights).
|
|
if allinteger:
|
|
verifyOptimum()
|
|
|
|
return matching_dict_to_set(mate)
|