You have tasks and people, courses and time slots, jobs and machines, and each item on the left can only go to certain items on the right. Can every left item get its own partner? Trying all assignments is hopeless, and a failed matching run only tells you 'no'. Hall's marriage theorem gives a much better answer: if the assignment is impossible, there is a specific small group on the left whose combined options are too few, and that group is a certificate anyone can check.
This article proves the theorem in the way that makes it algorithmic, turns the proof into a function that returns either a complete matching or a verified violating set, extends it to the deficiency formula for how many items can be placed, and walks through the corollaries that make Hall the tool behind regular-graph matchings, edge colouring and doubly stochastic matrices. For the algorithms that compute matchings quickly and the weighted variants, see the bipartite matching article.
The statement and the easy direction
Let G be a bipartite graph with left side L and right side R. For a set S of left vertices, write N(S) for the set of right vertices adjacent to at least one member of S. A matching is a set of edges with no shared endpoint; it saturates L if every left vertex is covered.
Hall's theorem (1935). G has a matching that saturates L if and only if |N(S)| >= |S| for every subset S of L.
The forward direction is immediate. If a saturating matching exists, it maps the members of any S to distinct right vertices, all of them in N(S), so N(S) has at least |S| members. The content of the theorem is the converse: no clever global obstruction exists. If every group of left vertices individually has enough options, a complete assignment exists. Equivalently, when no saturating matching exists, some S has |N(S)| < |S|, and that S is called a Hall violator.
The condition quantifies over 2 to the |L| subsets, so it is not something you check directly. Its value is that it converts a 'no' answer from a search into a short, independently verifiable explanation.
Why the condition suffices: the alternating tree
There are several proofs; the one worth knowing is the one through augmenting paths, because it is also the algorithm.
Take a maximum matching M and suppose some left vertex u is unmatched. An alternating path from u uses a non-matching edge from left to right, then a matching edge from right to left, and so on. Let Z be every vertex reachable from u along alternating paths, S the left vertices of Z and T the right vertices of Z.
- Every vertex of T is matched. If some reachable right vertex were free, the alternating path to it would be an augmenting path, and flipping it would enlarge M, contradicting maximality.
- The partner of every vertex in T is in S, because the matching edge extends the alternating path. Conversely every vertex of S other than u was reached through its partner. So S is u plus the partners of T, and
|S| = |T| + 1. - N(S) is exactly T. Any neighbour of a vertex in S is reachable by one more non-matching edge, so it lies in T; and every vertex of T was reached from S.
Therefore |N(S)| = |T| = |S| - 1 < |S|: the alternating tree grown from an unmatched vertex is a Hall violator. Contrapositive: if Hall's condition holds, a maximum matching leaves no left vertex unmatched. The violator is not necessarily the most deficient set in the graph: its deficiency is exactly one, while another set may fall further short. One violator is all a certificate needs.
Code: a matching or a certificate
The function below runs Kuhn's augmenting-path matching. When an augmentation from u fails, the sets of left and right vertices it visited are exactly S and T from the proof, so it returns them as the certificate. The verifier recomputes N(S) from the graph rather than trusting the search.
def hall_check(adj, n_right):
"""adj[u] lists right vertices for left vertex u.
Returns ("matching", match_left) or ("violator", S, N(S)) with |N(S)| < |S|."""
match_r = [-1] * n_right
match_l = [-1] * len(adj)
def augment(u, seen_l, seen_r):
seen_l.add(u)
for v in adj[u]:
if v in seen_r:
continue
seen_r.add(v)
if match_r[v] == -1 or augment(match_r[v], seen_l, seen_r):
match_r[v], match_l[u] = u, v
return True
return False
for u in range(len(adj)):
seen_l, seen_r = set(), set()
if not augment(u, seen_l, seen_r):
return ("violator", sorted(seen_l), sorted(seen_r))
return ("matching", match_l)
def verify(adj, result):
if result[0] == "matching":
m = result[1]
assert len(set(m)) == len(m) and all(m[u] in adj[u] for u in range(len(adj)))
else:
_, S, NS = result
assert set(NS) == {v for u in S for v in adj[u]}
assert len(NS) < len(S)A failed search changes nothing, so the matching is still valid afterwards, and the visited sets satisfy the three facts of the proof. The function returns at the first left vertex that cannot be placed. If you want the full maximum matching as well, keep going and collect one violator per failure.
Worked example: a weekend change rota
An infrastructure team has five changes for the weekend window and five engineers. Each change can only be run by people who hold the matching skill:
| Task | Qualified engineers |
|---|---|
| db-migrate | Ana, Ben |
| k8s-upgrade | Ben, Chen, Dev |
| cert-rotate | Ana, Ben |
| gpu-driver | Dev, Eli |
| dns-cutover | Ana, Ben |
Every task has at least two qualified people and every engineer is qualified for something, so the obvious checks pass. Running hall_check returned a violator: S = {db-migrate, cert-rotate, dns-cutover} with N(S) = {Ana, Ben}. Three tasks, two people. No scheduling cleverness can fix that, and the certificate says exactly what will: one more person qualified on at least one of those three tasks.
Training Chen on dns-cutover and rerunning produced a complete assignment: db-migrate to Ben, k8s-upgrade to Dev, cert-rotate to Ana, gpu-driver to Eli and dns-cutover to Chen. Note what the certificate bought you. A plain matcher would have said 'four of five', and someone would have had to guess which task to drop or whom to train. The violator turns a failure into an action.
The deficiency version
Hall's theorem has a quantitative form that tells you how many left vertices can be matched when not all can. Define the deficiency d = max over S of (|S| - |N(S)|), counting the empty set so d is at least 0. Then (Konig and Ore):
size of a maximum matching = |L| - dProof sketch: add d new right vertices adjacent to every left vertex. Every S now satisfies Hall's condition, so a saturating matching exists, and removing the at most d dummy edges leaves a matching of size at least |L| - d. In the other direction, the most deficient S can have at most |N(S)| of its members matched, so at least d left vertices are always unmatched.
The code above was cross-checked against this formula by brute force. Over 3,000 random bipartite graphs with up to 8 vertices per side and random edge density, the script enumerated every subset to compute d, confirmed that hall_check reports a matching exactly when d = 0, verified every returned certificate (1,832 of the graphs had a violator), and confirmed that the maximum matching size equals |L| - d in every case. The same quantity is the gap between |L| and the minimum vertex cover in the Konig's theorem article; Hall and Konig are two faces of one min-max duality, which is itself max-flow min-cut on the unit-capacity network.
Corollaries you will use
Much of Hall's usefulness comes from the results it proves in a few lines.
- Systems of distinct representatives. A family of sets A_1..A_n has a choice of distinct elements, one from each set, if and only if every k of the sets together contain at least k elements. This is Hall's theorem with the sets as left vertices; it is the form used for committee selection and for assigning unique identifiers from allowed pools.
- Regular bipartite graphs have perfect matchings. If every vertex has degree k at least 1, a set S sends exactly k|S| edges into N(S), and N(S) can absorb at most k|N(S)| of them, so |N(S)| is at least |S|. Remove the matching and the graph is (k-1)-regular, so repeat.
- Edge colouring. Repeating that argument after padding to a regular graph shows that a bipartite graph with maximum degree D can be edge-coloured with D colours (Konig's line colouring theorem). In scheduling terms: a set of pairwise meetings, each person in at most D of them, fits into D time slots with nobody double-booked.
- Latin rectangles extend. Any r by n Latin rectangle with r less than n can gain a row, because the 'which symbols can still go in which column' graph is regular. Repeating fills the full Latin square.
- Birkhoff-von Neumann. Hall applied to the support of a doubly stochastic matrix shows it contains a permutation; subtracting and repeating writes the matrix as a convex combination of permutation matrices, the basis of decomposing fractional assignments into schedules.
- Capacities. If right vertex v can take c(v) left vertices, the condition becomes: the total capacity of N(S) is at least |S|. Prove it by copying v c(v) times. Demands on the left work the same way.
Checking the condition at scale
Never test the condition by enumerating subsets; for 40 left vertices that is over a trillion of them. Compute a maximum matching instead and read the answer, plus a violator, off the result. Hopcroft-Karp runs in O(E sqrt(V)) and is described in the Hopcroft-Karp article. A max-flow formulation works as well: the left vertices reachable from the source in the final residual graph form a maximally deficient S, which is stronger than the single-tree violator above.
Three engineering notes. The recursive augment hits Python's default recursion limit of 1,000 on long alternating paths, so convert it to an explicit stack for graphs with thousands of vertices. Verification of a certificate is O(E) and should run in production even when the search is trusted, because it catches adjacency bugs. And when the input comes from a database, deduplicate edges and normalise identifiers first, or two spellings of one engineer will mask a violator.
Failure modes
- Checking only singletons. 'Every task has two qualified people' is the S of size one case; the worked example passes it and still fails.
- Confusing Hall with stable matching. Hall decides whether a complete assignment exists; it says nothing about preferences. For that, see the stable matching article.
- Applying the theorem to the wrong side. Saturating L and saturating R are different questions when |L| differs from |R|; state which side must be covered.
- Ignoring capacities. Modelling a person who can take two tasks as a single vertex produces false violators; replicate or use the capacity form.
- Assuming the violator is minimal or unique. The tree gives one valid certificate; other, smaller violating sets may exist and may be more useful to report.
- Using Hall on general graphs. Non-bipartite graphs need Tutte's condition and a blossom algorithm; the alternating-tree argument breaks on odd cycles.
What to do next
- Run
hall_checkon the worked example, then remove Chen's new skill and confirm the same violator returns. - Add the brute-force deficiency function and reproduce the 3,000-graph cross-check on your own random seed.
- Model one real allocation problem you own (on-call, rooms, GPUs to jobs) as a bipartite graph and report a violator, not just a count, when it fails.
- Rewrite
augmentwith an explicit stack and test it on a path-shaped graph with 10,000 vertices. - Replace the matcher with Hopcroft-Karp or max-flow and extract a maximally deficient set from the residual graph.
- Prove the regular-graph corollary yourself, then use it to schedule a round of pairwise meetings into the minimum number of slots.