Five applicants, to , and five jobs, to . An edge joins an applicant to each job they are qualified for:
Can every applicant get a different job they are qualified for? In the language of module 10: does have a matching that saturates , one that covers every vertex of ?
The answer is no. The best you can do is four. But "I tried and failed" is not an answer in a proof course. Someone has to be convinced that no assignment of all five exists, without watching you try all of them. This module is about that kind of evidence: a short object, a certificate, that proves the answer in both directions. A matching proves "yes". This module finds what proves "no".
Throughout, an -bigraph is a bipartite graph with a fixed bipartition . For , the neighborhood is the set of vertices of adjacent to at least one vertex of .
This module builds on module 10. If you have not taken it, here is all of it that is used below.
Berge's theorem (1957). A matching is maximum if and only if there is no -augmenting path.
Proof. If an augmenting path exists, swapping along it gives a larger matching, so is not maximum. Conversely, suppose is not maximum, and let be a larger matching. In the edges that belong to exactly one of and , every vertex has degree at most 2, so these edges form paths and even cycles that alternate between and . Cycles use equally many edges of each, and , so some path has more edges of than of . Its first and last edges are in , and its ends are unsaturated by : an -edge at an end would either lie in the symmetric difference too (so the path would continue) or belong to as well (so two -edges would meet at that end). So it is an -augmenting path.
There are only finitely many ways to assign jobs, so a computer can list them all:
from itertools import combinations, chain
H = {"a": {"1", "2"}, "b": {"1", "2"}, "c": {"1", "2", "3"}, "d": {"3"}, "e": {"3", "4", "5"}}
X = list(H)
Y = ["1", "2", "3", "4", "5"]
def all_matchings(adj, xs):
"""Every matching of an X,Y-bigraph, as dicts x -> y (brute force, for checking only)."""
found = []
def extend(i, used, current):
if i == len(xs):
found.append(dict(current))
return
extend(i + 1, used, current) # x_i stays unmatched
for y in sorted(adj[xs[i]] - used):
current[xs[i]] = y
extend(i + 1, used | {y}, current)
del current[xs[i]]
extend(0, set(), {})
return found
matchings = all_matchings(H, X)
assert max(len(m) for m in matchings) == 4
assert not any(len(m) == 5 for m in matchings)
assert sum(len(adj) for adj in H.values()) == 11
That settles , but it is not a proof anyone can check by reading. It is also hopeless at scale: with 30 applicants the list is astronomically long. And it wastes work in a specific way: it re-examines the same doomed corner of the graph in every assignment. Look at , , and . Between them they are qualified for only three jobs, . Four people cannot fill three jobs with one person each. That single observation rules out every assignment at once, and anyone can check it in a few seconds.
The observation generalizes. If a matching saturates , it sends the vertices of any to distinct vertices, all of them in . So:
Hall's condition. For every subset , .
A set with is a Hall violator. One violator is a certificate that no matching saturates , and checking it costs one neighborhood computation. That half is easy (it is the "only if" direction below). The theorem is the other half: violators are the only obstruction.
Hall's theorem (P. Hall, 1935)
An -bigraph has a matching saturating if and only if for every .
The quantifier "for every " is the whole content of the condition. Checking it for alone is not enough:
No. Take and with edges , , , , . Then has 3 vertices, but has : two applicants, one job. Quiz answers that state Hall's condition as "" lose the theorem.
In , a computer can check all nonempty subsets of :
def N(adj, S):
return set().union(*(adj[x] for x in S))
def subsets(xs):
return chain.from_iterable(combinations(xs, r) for r in range(1, len(xs) + 1))
violators = [set(S) for S in subsets(X) if len(N(H, S)) < len(S)]
assert violators == [{"a", "b", "c", "d"}]
assert N(H, {"a", "b", "c", "d"}) == {"1", "2", "3"}
So has exactly one violator, the set you spotted by eye. But checking all subsets is the same exponential search as before. Hall's theorem would be little use if finding a violator required it. The proof below shows it doesn't: the search that fails to grow a matching hands you the violator.
Start from a maximum matching. In one is , and is the only unsaturated vertex of . Recall from module 10 that an -alternating path uses edges that are alternately outside and inside , and that (Berge) is maximum exactly when no -augmenting path exists.
Explore from along alternating paths. From a vertex of , leave by any edge not in . From a vertex of , leave by its matching edge. Let be everything reached:
The search found no unsaturated vertex of , so no augmenting path starts at , as Berge promised for a maximum matching. Now read off and : it is exactly the violator, with .
M = {"a": "1", "b": "2", "c": "3", "e": "4"}
Z = {"d", "3", "c", "1", "a", "2", "b"} # the trace above
S = {v for v in Z if v in H}
T = Z - S
assert N(H, S) == T and len(S) == len(T) + 1
assert all(y in M.values() for y in T) # every vertex of T is saturated...
assert {x for x, y in M.items() if y in T} == S - {"d"} # ...by a partner in S
That was not luck. It is the proof.
Here is the proof written the way a graded solution should be. Read it once for the argument and once for how it is built.
Proof. Necessity. Suppose a matching saturates , and let . The -partners of the vertices of are distinct vertices, each adjacent to a vertex of , so .
Sufficiency. We prove the contrapositive: if no matching saturates , we exhibit a violator. Let be a maximum matching. It does not saturate , so some is unsaturated. Let be the set of vertices reachable from by -alternating paths, and let and .
So , and is a violator.
Proof moves used here. The same moves recur all semester:
A graph is -regular if every vertex has degree .
Corollary. For , every -regular -bigraph has a perfect matching.
Proof. Count edges from each side: every edge has one end in and one in , so , and since . So a matching saturating is perfect, and by Hall's theorem it suffices to check Hall's condition. Let . Exactly edges leave , and all of them end in . The vertices of have degree , so they absorb at most edge ends. Hence , and .
The proof counts one set of edges in two ways, from and from . Double counting is the standard way to verify Hall's condition for a whole family of graphs at once. Note what the proof uses: (for the count no longer forces , and an edgeless graph has no perfect matching anyway) and that every vertex of has degree at most . The argument works for multigraphs too, since it only counts edges.
A close relative of double counting is averaging. If two sums over the same index set satisfy , then for at least one , since otherwise adding over all would reverse the inequality. It does not follow for every : , yet .
The 3-cube (eight vertices, the 3-bit strings, adjacent when they differ in one bit) is 3-regular and bipartite, split by the parity of the number of ones, so it has a perfect matching:
Q3 = {v: {v ^ (1 << i) for i in range(3)} for v in range(8)}
even = [v for v in Q3 if bin(v).count("1") % 2 == 0]
perfect = [m for m in all_matchings(Q3, even) if len(m) == 4]
assert all(len(Q3[v]) == 3 for v in Q3) and len(perfect) == 9
A vertex cover is a set of vertices touching every edge. Write for the size of a maximum matching and for the size of a minimum vertex cover. In every graph : the edges of a matching share no vertex, so a cover needs a different vertex for each of them.
So a matching and a cover of the same size certify each other: the matching is maximum and the cover is minimum. For bipartite graphs such a pair always exists:
König–Egerváry theorem (1931)
In every bipartite graph, : the maximum size of a matching equals the minimum size of a vertex cover.
Proof. Let be a maximum matching of an -bigraph , let be the set of -unsaturated vertices of , and let be the set of vertices reachable by -alternating paths starting anywhere in . Let , , and
Claim 1: is a vertex cover. Take an edge with . If , then . If , then , and step 3 of Hall's proof (which works for grown from all of ) gives .
Claim 2: has at most vertices. By step 1, every vertex of is saturated, and by step 2 its partner lies in . Every vertex of is saturated too, because . So each vertex of is saturated, and no edge of has both ends in : an edge of with its end in has its end in . So distinct vertices of lie on distinct edges of .
Conclusion. , so equality holds throughout: and certify each other.
In , and the trace above gives , four vertices for four matching edges:
Q = (set(X) - S) | T
edges = [(x, y) for x in H for y in H[x]]
assert Q == {"e", "1", "2", "3"} and all(x in Q or y in Q for x, y in edges)
assert len(Q) == len(M) == 4
Not in general. With two unsaturated vertices and , a grown from alone leaves outside , so lands in even though it is unsaturated, and the count breaks. In the two choices coincide only because is the one unsaturated vertex.
Bipartiteness is needed. In the triangle , but every cover needs two vertices.
Not needed for the lab. Two more parameters complete the picture: , the size of a largest independent set (no two adjacent), and , the size of a smallest edge cover (edges touching every vertex). Gallai's identities hold in every graph with vertices:
With König–Egerváry, a bipartite graph without isolated vertices has . In , :
| 4 | 4 | 6 | 6 |
V = X + Y
def is_independent(q):
return all(not (x in q and y in q) for x, y in edges)
alpha = max(k for k in range(len(V) + 1) for q in combinations(V, k) if is_independent(set(q)))
beta_prime = min(k for k in range(1, len(edges) + 1)
for f in combinations(edges, k) if set(chain.from_iterable(f)) == set(V))
assert (alpha, beta_prime) == (6, 6) and alpha + 4 == 10 and 4 + beta_prime == 10
The theorems turn an exponential question into short evidence that is cheap to check, in both directions. For , compare the number of subsets Hall's condition quantifies over, , with the size of a violator certificate (at most vertices: and ) and of a matching–cover pair (at most edges and vertices):
| subsets | violator | pair | |
|---|---|---|---|
| 5 | 31 | ||
| 20 | 1,048,575 | ||
| 50 | about |
assert 2 ** 5 - 1 == 31 and 2 ** 20 - 1 == 1_048_575
assert round((2 ** 50 - 1) / 1e15, 2) == 1.13
Finding the certificate is also cheap. Each alternating search looks at each edge at most once, so growing a maximum matching one augmenting path at a time takes at most searches: time in the worst case (Kuhn's method, the bipartite case of module 10's augmenting paths). Hopcroft and Karp's algorithm improves this to . Checking a certificate costs one pass over the edges.
On a board, some squares are blocked. You want to place six rooks on open squares so that no two share a row or a column. Sometimes it is impossible. When it is, what short evidence would convince a skeptic, and why must such evidence always exist? Nothing in the question mentions applicants or jobs. The lab's last exercise is a different problem that also hides its structure, and it will not say which idea it needs either.
The lab runs in your browser. Your code grows the alternating search frame by frame and reads a violator off where it stops, then builds a minimum cover and sees a tempting shortcut fail. Half of the lab is proofs: assemble one from its steps (with decoys), find the false step in a wrong one, and write one for a problem that names no technique.
maximum_bipartite_matching (in scipy.sparse.csgraph) uses
Hopcroft–Karp; NetworkX's bipartite.to_vertex_cover builds the König cover from a maximum
matching.After the lab, the tutor will ask you to defend your work out loud:
Topic list modeled on the public syllabus of UIUC Math 412 (Graph Theory), which follows D. B. West, Introduction to Graph Theory, 2nd ed., Chapters 1–7. All lessons, proofs, examples, code and exercises here are original. This course is independent and not affiliated with or endorsed by the University of Illinois.