Source code for mathematicskit.combinatorics.systems.matching
r"""Bipartite matchings and Hall's marriage theorem.
A bipartite graph has a matching that covers every left vertex exactly
when every set :math:`S` of left vertices has at least :math:`|S|`
neighbors. Hand-rolled (augmenting paths): the counting condition is the
subject here. For a fast library matching on large graphs see
:func:`scipy.sparse.csgraph.maximum_bipartite_matching`. See P. Hall,
"On Representatives of Subsets," Journal of the London Mathematical
Society s1-10(1) (1935), 26-30.
"""
from __future__ import annotations
from itertools import combinations
from mathematicskit.combinatorics.core.base import HallResult
__all__ = ["maximum_matching", "hall_condition"]
[docs]
def maximum_matching(adjacency: dict) -> dict:
r"""A maximum matching of a bipartite graph, by repeated augmenting-path search.
Parameters
----------
adjacency : dict
``{left_vertex: iterable of right vertices}``.
Returns
-------
dict
``{left: right}`` for every matched left vertex.
Examples
--------
>>> sorted(maximum_matching({"a": [1, 2], "b": [1], "c": [2, 3]}).items())
[('a', 2), ('b', 1), ('c', 3)]
"""
match_of_right = {}
def augment(u, visited) -> bool:
for v in adjacency[u]:
if v in visited:
continue
visited.add(v)
if v not in match_of_right or augment(match_of_right[v], visited):
match_of_right[v] = u
return True
return False
for u in adjacency:
augment(u, set())
return {u: v for v, u in match_of_right.items()}
[docs]
def hall_condition(adjacency: dict) -> HallResult:
r"""Check Hall's condition :math:`|N(S)| \ge |S|` for every subset :math:`S` of the left side.
Checks all :math:`2^n` subsets, so it is meant for small
illustrations. Hall's theorem says the condition holds exactly when
:func:`maximum_matching` covers every left vertex; this function
returns both, so the two can be compared.
Parameters
----------
adjacency : dict
``{left_vertex: iterable of right vertices}``.
Returns
-------
HallResult
Examples
--------
>>> hall_condition({"a": [1], "b": [1], "c": [2]}).violating_subset == frozenset({"a", "b"})
True
"""
left = list(adjacency)
matching = maximum_matching(adjacency)
for size in range(1, len(left) + 1):
for subset in combinations(left, size):
neighbors = set().union(*(adjacency[u] for u in subset))
if len(neighbors) < size:
return HallResult(satisfied=False, violating_subset=frozenset(subset), matching=matching)
return HallResult(satisfied=True, matching=matching)