583 lines
21 KiB
Python
583 lines
21 KiB
Python
|
# This module uses material from the Wikipedia article Hopcroft--Karp algorithm
|
|||
|
# <https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm>, accessed on
|
|||
|
# January 3, 2015, which is released under the Creative Commons
|
|||
|
# Attribution-Share-Alike License 3.0
|
|||
|
# <http://creativecommons.org/licenses/by-sa/3.0/>. That article includes
|
|||
|
# pseudocode, which has been translated into the corresponding Python code.
|
|||
|
#
|
|||
|
# Portions of this module use code from David Eppstein's Python Algorithms and
|
|||
|
# Data Structures (PADS) library, which is dedicated to the public domain (for
|
|||
|
# proof, see <http://www.ics.uci.edu/~eppstein/PADS/ABOUT-PADS.txt>).
|
|||
|
"""Provides functions for computing maximum cardinality matchings and minimum
|
|||
|
weight full matchings in a bipartite graph.
|
|||
|
|
|||
|
If you don't care about the particular implementation of the maximum matching
|
|||
|
algorithm, simply use the :func:`maximum_matching`. If you do care, you can
|
|||
|
import one of the named maximum matching algorithms directly.
|
|||
|
|
|||
|
For example, to find a maximum matching in the complete bipartite graph with
|
|||
|
two vertices on the left and three vertices on the right:
|
|||
|
|
|||
|
>>> G = nx.complete_bipartite_graph(2, 3)
|
|||
|
>>> left, right = nx.bipartite.sets(G)
|
|||
|
>>> list(left)
|
|||
|
[0, 1]
|
|||
|
>>> list(right)
|
|||
|
[2, 3, 4]
|
|||
|
>>> nx.bipartite.maximum_matching(G)
|
|||
|
{0: 2, 1: 3, 2: 0, 3: 1}
|
|||
|
|
|||
|
The dictionary returned by :func:`maximum_matching` includes a mapping for
|
|||
|
vertices in both the left and right vertex sets.
|
|||
|
|
|||
|
Similarly, :func:`minimum_weight_full_matching` produces, for a complete
|
|||
|
weighted bipartite graph, a matching whose cardinality is the cardinality of
|
|||
|
the smaller of the two partitions, and for which the sum of the weights of the
|
|||
|
edges included in the matching is minimal.
|
|||
|
|
|||
|
"""
|
|||
|
import collections
|
|||
|
import itertools
|
|||
|
|
|||
|
from networkx.algorithms.bipartite.matrix import biadjacency_matrix
|
|||
|
from networkx.algorithms.bipartite import sets as bipartite_sets
|
|||
|
import networkx as nx
|
|||
|
|
|||
|
__all__ = [
|
|||
|
"maximum_matching",
|
|||
|
"hopcroft_karp_matching",
|
|||
|
"eppstein_matching",
|
|||
|
"to_vertex_cover",
|
|||
|
"minimum_weight_full_matching",
|
|||
|
]
|
|||
|
|
|||
|
INFINITY = float("inf")
|
|||
|
|
|||
|
|
|||
|
def hopcroft_karp_matching(G, top_nodes=None):
|
|||
|
"""Returns the maximum cardinality matching of the bipartite graph `G`.
|
|||
|
|
|||
|
A matching is a set of edges that do not share any nodes. A maximum
|
|||
|
cardinality matching is a matching with the most edges possible. It
|
|||
|
is not always unique. Finding a matching in a bipartite graph can be
|
|||
|
treated as a networkx flow problem.
|
|||
|
|
|||
|
The functions ``hopcroft_karp_matching`` and ``maximum_matching``
|
|||
|
are aliases of the same function.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
G : NetworkX graph
|
|||
|
|
|||
|
Undirected bipartite graph
|
|||
|
|
|||
|
top_nodes : container of nodes
|
|||
|
|
|||
|
Container with all nodes in one bipartite node set. If not supplied
|
|||
|
it will be computed. But if more than one solution exists an exception
|
|||
|
will be raised.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
matches : dictionary
|
|||
|
|
|||
|
The matching is returned as a dictionary, `matches`, such that
|
|||
|
``matches[v] == w`` if node `v` is matched to node `w`. Unmatched
|
|||
|
nodes do not occur as a key in `matches`.
|
|||
|
|
|||
|
Raises
|
|||
|
------
|
|||
|
AmbiguousSolution
|
|||
|
Raised if the input bipartite graph is disconnected and no container
|
|||
|
with all nodes in one bipartite set is provided. When determining
|
|||
|
the nodes in each bipartite set more than one valid solution is
|
|||
|
possible if the input graph is disconnected.
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
This function is implemented with the `Hopcroft--Karp matching algorithm
|
|||
|
<https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm>`_ for
|
|||
|
bipartite graphs.
|
|||
|
|
|||
|
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
|||
|
for further details on how bipartite graphs are handled in NetworkX.
|
|||
|
|
|||
|
See Also
|
|||
|
--------
|
|||
|
maximum_matching
|
|||
|
hopcroft_karp_matching
|
|||
|
eppstein_matching
|
|||
|
|
|||
|
References
|
|||
|
----------
|
|||
|
.. [1] John E. Hopcroft and Richard M. Karp. "An n^{5 / 2} Algorithm for
|
|||
|
Maximum Matchings in Bipartite Graphs" In: **SIAM Journal of Computing**
|
|||
|
2.4 (1973), pp. 225--231. <https://doi.org/10.1137/0202019>.
|
|||
|
|
|||
|
"""
|
|||
|
# First we define some auxiliary search functions.
|
|||
|
#
|
|||
|
# If you are a human reading these auxiliary search functions, the "global"
|
|||
|
# variables `leftmatches`, `rightmatches`, `distances`, etc. are defined
|
|||
|
# below the functions, so that they are initialized close to the initial
|
|||
|
# invocation of the search functions.
|
|||
|
def breadth_first_search():
|
|||
|
for v in left:
|
|||
|
if leftmatches[v] is None:
|
|||
|
distances[v] = 0
|
|||
|
queue.append(v)
|
|||
|
else:
|
|||
|
distances[v] = INFINITY
|
|||
|
distances[None] = INFINITY
|
|||
|
while queue:
|
|||
|
v = queue.popleft()
|
|||
|
if distances[v] < distances[None]:
|
|||
|
for u in G[v]:
|
|||
|
if distances[rightmatches[u]] is INFINITY:
|
|||
|
distances[rightmatches[u]] = distances[v] + 1
|
|||
|
queue.append(rightmatches[u])
|
|||
|
return distances[None] is not INFINITY
|
|||
|
|
|||
|
def depth_first_search(v):
|
|||
|
if v is not None:
|
|||
|
for u in G[v]:
|
|||
|
if distances[rightmatches[u]] == distances[v] + 1:
|
|||
|
if depth_first_search(rightmatches[u]):
|
|||
|
rightmatches[u] = v
|
|||
|
leftmatches[v] = u
|
|||
|
return True
|
|||
|
distances[v] = INFINITY
|
|||
|
return False
|
|||
|
return True
|
|||
|
|
|||
|
# Initialize the "global" variables that maintain state during the search.
|
|||
|
left, right = bipartite_sets(G, top_nodes)
|
|||
|
leftmatches = {v: None for v in left}
|
|||
|
rightmatches = {v: None for v in right}
|
|||
|
distances = {}
|
|||
|
queue = collections.deque()
|
|||
|
|
|||
|
# Implementation note: this counter is incremented as pairs are matched but
|
|||
|
# it is currently not used elsewhere in the computation.
|
|||
|
num_matched_pairs = 0
|
|||
|
while breadth_first_search():
|
|||
|
for v in left:
|
|||
|
if leftmatches[v] is None:
|
|||
|
if depth_first_search(v):
|
|||
|
num_matched_pairs += 1
|
|||
|
|
|||
|
# Strip the entries matched to `None`.
|
|||
|
leftmatches = {k: v for k, v in leftmatches.items() if v is not None}
|
|||
|
rightmatches = {k: v for k, v in rightmatches.items() if v is not None}
|
|||
|
|
|||
|
# At this point, the left matches and the right matches are inverses of one
|
|||
|
# another. In other words,
|
|||
|
#
|
|||
|
# leftmatches == {v, k for k, v in rightmatches.items()}
|
|||
|
#
|
|||
|
# Finally, we combine both the left matches and right matches.
|
|||
|
return dict(itertools.chain(leftmatches.items(), rightmatches.items()))
|
|||
|
|
|||
|
|
|||
|
def eppstein_matching(G, top_nodes=None):
|
|||
|
"""Returns the maximum cardinality matching of the bipartite graph `G`.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
G : NetworkX graph
|
|||
|
|
|||
|
Undirected bipartite graph
|
|||
|
|
|||
|
top_nodes : container
|
|||
|
|
|||
|
Container with all nodes in one bipartite node set. If not supplied
|
|||
|
it will be computed. But if more than one solution exists an exception
|
|||
|
will be raised.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
matches : dictionary
|
|||
|
|
|||
|
The matching is returned as a dictionary, `matching`, such that
|
|||
|
``matching[v] == w`` if node `v` is matched to node `w`. Unmatched
|
|||
|
nodes do not occur as a key in `matching`.
|
|||
|
|
|||
|
Raises
|
|||
|
------
|
|||
|
AmbiguousSolution
|
|||
|
Raised if the input bipartite graph is disconnected and no container
|
|||
|
with all nodes in one bipartite set is provided. When determining
|
|||
|
the nodes in each bipartite set more than one valid solution is
|
|||
|
possible if the input graph is disconnected.
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
This function is implemented with David Eppstein's version of the algorithm
|
|||
|
Hopcroft--Karp algorithm (see :func:`hopcroft_karp_matching`), which
|
|||
|
originally appeared in the `Python Algorithms and Data Structures library
|
|||
|
(PADS) <http://www.ics.uci.edu/~eppstein/PADS/ABOUT-PADS.txt>`_.
|
|||
|
|
|||
|
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
|||
|
for further details on how bipartite graphs are handled in NetworkX.
|
|||
|
|
|||
|
See Also
|
|||
|
--------
|
|||
|
|
|||
|
hopcroft_karp_matching
|
|||
|
|
|||
|
"""
|
|||
|
# Due to its original implementation, a directed graph is needed
|
|||
|
# so that the two sets of bipartite nodes can be distinguished
|
|||
|
left, right = bipartite_sets(G, top_nodes)
|
|||
|
G = nx.DiGraph(G.edges(left))
|
|||
|
# initialize greedy matching (redundant, but faster than full search)
|
|||
|
matching = {}
|
|||
|
for u in G:
|
|||
|
for v in G[u]:
|
|||
|
if v not in matching:
|
|||
|
matching[v] = u
|
|||
|
break
|
|||
|
while True:
|
|||
|
# structure residual graph into layers
|
|||
|
# pred[u] gives the neighbor in the previous layer for u in U
|
|||
|
# preds[v] gives a list of neighbors in the previous layer for v in V
|
|||
|
# unmatched gives a list of unmatched vertices in final layer of V,
|
|||
|
# and is also used as a flag value for pred[u] when u is in the first
|
|||
|
# layer
|
|||
|
preds = {}
|
|||
|
unmatched = []
|
|||
|
pred = {u: unmatched for u in G}
|
|||
|
for v in matching:
|
|||
|
del pred[matching[v]]
|
|||
|
layer = list(pred)
|
|||
|
|
|||
|
# repeatedly extend layering structure by another pair of layers
|
|||
|
while layer and not unmatched:
|
|||
|
newLayer = {}
|
|||
|
for u in layer:
|
|||
|
for v in G[u]:
|
|||
|
if v not in preds:
|
|||
|
newLayer.setdefault(v, []).append(u)
|
|||
|
layer = []
|
|||
|
for v in newLayer:
|
|||
|
preds[v] = newLayer[v]
|
|||
|
if v in matching:
|
|||
|
layer.append(matching[v])
|
|||
|
pred[matching[v]] = v
|
|||
|
else:
|
|||
|
unmatched.append(v)
|
|||
|
|
|||
|
# did we finish layering without finding any alternating paths?
|
|||
|
if not unmatched:
|
|||
|
unlayered = {}
|
|||
|
for u in G:
|
|||
|
# TODO Why is extra inner loop necessary?
|
|||
|
for v in G[u]:
|
|||
|
if v not in preds:
|
|||
|
unlayered[v] = None
|
|||
|
# TODO Originally, this function returned a three-tuple:
|
|||
|
#
|
|||
|
# return (matching, list(pred), list(unlayered))
|
|||
|
#
|
|||
|
# For some reason, the documentation for this function
|
|||
|
# indicated that the second and third elements of the returned
|
|||
|
# three-tuple would be the vertices in the left and right vertex
|
|||
|
# sets, respectively, that are also in the maximum independent set.
|
|||
|
# However, what I think the author meant was that the second
|
|||
|
# element is the list of vertices that were unmatched and the third
|
|||
|
# element was the list of vertices that were matched. Since that
|
|||
|
# seems to be the case, they don't really need to be returned,
|
|||
|
# since that information can be inferred from the matching
|
|||
|
# dictionary.
|
|||
|
|
|||
|
# All the matched nodes must be a key in the dictionary
|
|||
|
for key in matching.copy():
|
|||
|
matching[matching[key]] = key
|
|||
|
return matching
|
|||
|
|
|||
|
# recursively search backward through layers to find alternating paths
|
|||
|
# recursion returns true if found path, false otherwise
|
|||
|
def recurse(v):
|
|||
|
if v in preds:
|
|||
|
L = preds.pop(v)
|
|||
|
for u in L:
|
|||
|
if u in pred:
|
|||
|
pu = pred.pop(u)
|
|||
|
if pu is unmatched or recurse(pu):
|
|||
|
matching[v] = u
|
|||
|
return True
|
|||
|
return False
|
|||
|
|
|||
|
for v in unmatched:
|
|||
|
recurse(v)
|
|||
|
|
|||
|
|
|||
|
def _is_connected_by_alternating_path(G, v, matched_edges, unmatched_edges, targets):
|
|||
|
"""Returns True if and only if the vertex `v` is connected to one of
|
|||
|
the target vertices by an alternating path in `G`.
|
|||
|
|
|||
|
An *alternating path* is a path in which every other edge is in the
|
|||
|
specified maximum matching (and the remaining edges in the path are not in
|
|||
|
the matching). An alternating path may have matched edges in the even
|
|||
|
positions or in the odd positions, as long as the edges alternate between
|
|||
|
'matched' and 'unmatched'.
|
|||
|
|
|||
|
`G` is an undirected bipartite NetworkX graph.
|
|||
|
|
|||
|
`v` is a vertex in `G`.
|
|||
|
|
|||
|
`matched_edges` is a set of edges present in a maximum matching in `G`.
|
|||
|
|
|||
|
`unmatched_edges` is a set of edges not present in a maximum
|
|||
|
matching in `G`.
|
|||
|
|
|||
|
`targets` is a set of vertices.
|
|||
|
|
|||
|
"""
|
|||
|
|
|||
|
def _alternating_dfs(u, along_matched=True):
|
|||
|
"""Returns True if and only if `u` is connected to one of the
|
|||
|
targets by an alternating path.
|
|||
|
|
|||
|
`u` is a vertex in the graph `G`.
|
|||
|
|
|||
|
If `along_matched` is True, this step of the depth-first search
|
|||
|
will continue only through edges in the given matching. Otherwise, it
|
|||
|
will continue only through edges *not* in the given matching.
|
|||
|
|
|||
|
"""
|
|||
|
if along_matched:
|
|||
|
edges = itertools.cycle([matched_edges, unmatched_edges])
|
|||
|
else:
|
|||
|
edges = itertools.cycle([unmatched_edges, matched_edges])
|
|||
|
visited = set()
|
|||
|
stack = [(u, iter(G[u]), next(edges))]
|
|||
|
while stack:
|
|||
|
parent, children, valid_edges = stack[-1]
|
|||
|
try:
|
|||
|
child = next(children)
|
|||
|
if child not in visited:
|
|||
|
if (parent, child) in valid_edges or (child, parent) in valid_edges:
|
|||
|
if child in targets:
|
|||
|
return True
|
|||
|
visited.add(child)
|
|||
|
stack.append((child, iter(G[child]), next(edges)))
|
|||
|
except StopIteration:
|
|||
|
stack.pop()
|
|||
|
return False
|
|||
|
|
|||
|
# Check for alternating paths starting with edges in the matching, then
|
|||
|
# check for alternating paths starting with edges not in the
|
|||
|
# matching.
|
|||
|
return _alternating_dfs(v, along_matched=True) or _alternating_dfs(
|
|||
|
v, along_matched=False
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
def _connected_by_alternating_paths(G, matching, targets):
|
|||
|
"""Returns the set of vertices that are connected to one of the target
|
|||
|
vertices by an alternating path in `G` or are themselves a target.
|
|||
|
|
|||
|
An *alternating path* is a path in which every other edge is in the
|
|||
|
specified maximum matching (and the remaining edges in the path are not in
|
|||
|
the matching). An alternating path may have matched edges in the even
|
|||
|
positions or in the odd positions, as long as the edges alternate between
|
|||
|
'matched' and 'unmatched'.
|
|||
|
|
|||
|
`G` is an undirected bipartite NetworkX graph.
|
|||
|
|
|||
|
`matching` is a dictionary representing a maximum matching in `G`, as
|
|||
|
returned by, for example, :func:`maximum_matching`.
|
|||
|
|
|||
|
`targets` is a set of vertices.
|
|||
|
|
|||
|
"""
|
|||
|
# Get the set of matched edges and the set of unmatched edges. Only include
|
|||
|
# one version of each undirected edge (for example, include edge (1, 2) but
|
|||
|
# not edge (2, 1)). Using frozensets as an intermediary step we do not
|
|||
|
# require nodes to be orderable.
|
|||
|
edge_sets = {frozenset((u, v)) for u, v in matching.items()}
|
|||
|
matched_edges = {tuple(edge) for edge in edge_sets}
|
|||
|
unmatched_edges = {
|
|||
|
(u, v) for (u, v) in G.edges() if frozenset((u, v)) not in edge_sets
|
|||
|
}
|
|||
|
|
|||
|
return {
|
|||
|
v
|
|||
|
for v in G
|
|||
|
if v in targets
|
|||
|
or _is_connected_by_alternating_path(
|
|||
|
G, v, matched_edges, unmatched_edges, targets
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
def to_vertex_cover(G, matching, top_nodes=None):
|
|||
|
"""Returns the minimum vertex cover corresponding to the given maximum
|
|||
|
matching of the bipartite graph `G`.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
G : NetworkX graph
|
|||
|
|
|||
|
Undirected bipartite graph
|
|||
|
|
|||
|
matching : dictionary
|
|||
|
|
|||
|
A dictionary whose keys are vertices in `G` and whose values are the
|
|||
|
distinct neighbors comprising the maximum matching for `G`, as returned
|
|||
|
by, for example, :func:`maximum_matching`. The dictionary *must*
|
|||
|
represent the maximum matching.
|
|||
|
|
|||
|
top_nodes : container
|
|||
|
|
|||
|
Container with all nodes in one bipartite node set. If not supplied
|
|||
|
it will be computed. But if more than one solution exists an exception
|
|||
|
will be raised.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
vertex_cover : :class:`set`
|
|||
|
|
|||
|
The minimum vertex cover in `G`.
|
|||
|
|
|||
|
Raises
|
|||
|
------
|
|||
|
AmbiguousSolution
|
|||
|
Raised if the input bipartite graph is disconnected and no container
|
|||
|
with all nodes in one bipartite set is provided. When determining
|
|||
|
the nodes in each bipartite set more than one valid solution is
|
|||
|
possible if the input graph is disconnected.
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
This function is implemented using the procedure guaranteed by `Konig's
|
|||
|
theorem
|
|||
|
<https://en.wikipedia.org/wiki/K%C3%B6nig%27s_theorem_%28graph_theory%29>`_,
|
|||
|
which proves an equivalence between a maximum matching and a minimum vertex
|
|||
|
cover in bipartite graphs.
|
|||
|
|
|||
|
Since a minimum vertex cover is the complement of a maximum independent set
|
|||
|
for any graph, one can compute the maximum independent set of a bipartite
|
|||
|
graph this way:
|
|||
|
|
|||
|
>>> G = nx.complete_bipartite_graph(2, 3)
|
|||
|
>>> matching = nx.bipartite.maximum_matching(G)
|
|||
|
>>> vertex_cover = nx.bipartite.to_vertex_cover(G, matching)
|
|||
|
>>> independent_set = set(G) - vertex_cover
|
|||
|
>>> print(list(independent_set))
|
|||
|
[2, 3, 4]
|
|||
|
|
|||
|
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
|||
|
for further details on how bipartite graphs are handled in NetworkX.
|
|||
|
|
|||
|
"""
|
|||
|
# This is a Python implementation of the algorithm described at
|
|||
|
# <https://en.wikipedia.org/wiki/K%C3%B6nig%27s_theorem_%28graph_theory%29#Proof>.
|
|||
|
L, R = bipartite_sets(G, top_nodes)
|
|||
|
# Let U be the set of unmatched vertices in the left vertex set.
|
|||
|
unmatched_vertices = set(G) - set(matching)
|
|||
|
U = unmatched_vertices & L
|
|||
|
# Let Z be the set of vertices that are either in U or are connected to U
|
|||
|
# by alternating paths.
|
|||
|
Z = _connected_by_alternating_paths(G, matching, U)
|
|||
|
# At this point, every edge either has a right endpoint in Z or a left
|
|||
|
# endpoint not in Z. This gives us the vertex cover.
|
|||
|
return (L - Z) | (R & Z)
|
|||
|
|
|||
|
|
|||
|
#: Returns the maximum cardinality matching in the given bipartite graph.
|
|||
|
#:
|
|||
|
#: This function is simply an alias for :func:`hopcroft_karp_matching`.
|
|||
|
maximum_matching = hopcroft_karp_matching
|
|||
|
|
|||
|
|
|||
|
def minimum_weight_full_matching(G, top_nodes=None, weight="weight"):
|
|||
|
r"""Returns a minimum weight full matching of the bipartite graph `G`.
|
|||
|
|
|||
|
Let :math:`G = ((U, V), E)` be a weighted bipartite graph with real weights
|
|||
|
:math:`w : E \to \mathbb{R}`. This function then produces a matching
|
|||
|
:math:`M \subseteq E` with cardinality
|
|||
|
|
|||
|
.. math::
|
|||
|
\lvert M \rvert = \min(\lvert U \rvert, \lvert V \rvert),
|
|||
|
|
|||
|
which minimizes the sum of the weights of the edges included in the
|
|||
|
matching, :math:`\sum_{e \in M} w(e)`, or raises an error if no such
|
|||
|
matching exists.
|
|||
|
|
|||
|
When :math:`\lvert U \rvert = \lvert V \rvert`, this is commonly
|
|||
|
referred to as a perfect matching; here, since we allow
|
|||
|
:math:`\lvert U \rvert` and :math:`\lvert V \rvert` to differ, we
|
|||
|
follow Karp [1]_ and refer to the matching as *full*.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
G : NetworkX graph
|
|||
|
|
|||
|
Undirected bipartite graph
|
|||
|
|
|||
|
top_nodes : container
|
|||
|
|
|||
|
Container with all nodes in one bipartite node set. If not supplied
|
|||
|
it will be computed.
|
|||
|
|
|||
|
weight : string, optional (default='weight')
|
|||
|
|
|||
|
The edge data key used to provide each value in the matrix.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
matches : dictionary
|
|||
|
|
|||
|
The matching is returned as a dictionary, `matches`, such that
|
|||
|
``matches[v] == w`` if node `v` is matched to node `w`. Unmatched
|
|||
|
nodes do not occur as a key in `matches`.
|
|||
|
|
|||
|
Raises
|
|||
|
------
|
|||
|
ValueError
|
|||
|
Raised if no full matching exists.
|
|||
|
|
|||
|
ImportError
|
|||
|
Raised if SciPy is not available.
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
The problem of determining a minimum weight full matching is also known as
|
|||
|
the rectangular linear assignment problem. This implementation defers the
|
|||
|
calculation of the assignment to SciPy.
|
|||
|
|
|||
|
References
|
|||
|
----------
|
|||
|
.. [1] Richard Manning Karp:
|
|||
|
An algorithm to Solve the m x n Assignment Problem in Expected Time
|
|||
|
O(mn log n).
|
|||
|
Networks, 10(2):143–152, 1980.
|
|||
|
|
|||
|
"""
|
|||
|
try:
|
|||
|
import numpy as np
|
|||
|
import scipy.optimize
|
|||
|
except ImportError as e:
|
|||
|
raise ImportError(
|
|||
|
"minimum_weight_full_matching requires SciPy: " + "https://scipy.org/"
|
|||
|
) from e
|
|||
|
left, right = nx.bipartite.sets(G, top_nodes)
|
|||
|
U = list(left)
|
|||
|
V = list(right)
|
|||
|
# We explicitly create the biadjancency matrix having infinities
|
|||
|
# where edges are missing (as opposed to zeros, which is what one would
|
|||
|
# get by using toarray on the sparse matrix).
|
|||
|
weights_sparse = biadjacency_matrix(
|
|||
|
G, row_order=U, column_order=V, weight=weight, format="coo"
|
|||
|
)
|
|||
|
weights = np.full(weights_sparse.shape, np.inf)
|
|||
|
weights[weights_sparse.row, weights_sparse.col] = weights_sparse.data
|
|||
|
left_matches = scipy.optimize.linear_sum_assignment(weights)
|
|||
|
d = {U[u]: V[v] for u, v in zip(*left_matches)}
|
|||
|
# d will contain the matching from edges in left to right; we need to
|
|||
|
# add the ones from right to left as well.
|
|||
|
d.update({v: u for u, v in d.items()})
|
|||
|
return d
|