On this page

General Matchings

Twelve students and six double rooms

A residence hall has twelve students and six double rooms. The students have said who they would share with: thirteen pairs, written as edges of a graph L on the vertices s,a,b,c,d,e,f,p,q,t,u,w. Five rooms are already assigned: a with b, c with d, e with f, p with q and u with w. Students s and t have no room, and they did not agree to share with each other.

s a b c d e f p q t u w
The twelve students and the thirteen pairs who agreed to share. Blue: the five rooms already assigned (the matching M). Pink: s and t, still without a room.

Can the hall reshuffle the rooms so that all twelve students get one? And if it can't, what could it show the two students left over, to convince them that nobody was careless?

A set of edges in which no vertex appears twice is a matching, and a vertex on none of its edges is exposed. The question asks for a maximum matching in a graph with no two sides. Network Flow and Matchings matched people to jobs through a flow network; here any student may share with any other, so there is no source side and no sink side. This module assumes that module: augmenting paths, and answers that come with a certificate.

L = {}
for pair in "s-a a-b b-c c-d d-e e-f f-b c-p p-q q-t t-u u-w w-a".split():
    x, y = pair.split("-")
    L.setdefault(x, set()).add(y)
    L.setdefault(y, set()).add(x)

def edge(x, y):
    return frozenset((x, y))

M = {edge(*r) for r in ("ab", "cd", "ef", "pq", "uw")}

def is_matching(G, edges):
    ends = [v for e in edges for v in e]
    return (len(ends) == len(set(ends))
            and all(y in G[x] for x, y in map(tuple, edges)))

assert len(L) == 12 and sum(map(len, L.values())) == 2 * 13
assert is_matching(L, M)
assert sorted(v for v in L if not any(v in e for e in M)) == ["s", "t"]

Paths that improve a matching

The tool from Network Flow and Matchings carries over. A path is alternating for M if its edges are in turn outside and inside M. It is augmenting if it is also simple, joins two different exposed vertices, and so starts and ends with an edge outside M. Swapping the path's edges in and out, M⊕P, gives a matching with one more edge: every inner vertex of the path trades one matched edge for another, and the two exposed ends each gain one.

Theorem (Berge, 1957)

In any graph, a matching M is maximum if and only if no path is augmenting for M.

Proof. If an augmenting path exists, M⊕P is larger, so M is not maximum. Conversely, suppose some matching M* has more edges than M. In M⊕M* every vertex has at most one edge from each, so each connected piece is a path or a cycle whose edges alternate between M and M*. A cycle alternates, so it has as many edges of each. M* has more edges overall, so some piece has more M* edges than M edges. That piece is a path that starts and ends with an M* edge, and its two ends are exposed in M: each end has no second edge in the piece, and an M edge at that end would belong to the piece. So it is augmenting for M. ◻

So the algorithm is the same loop as in any graph: find an augmenting path, flip it, repeat. L has a perfect matching, so an augmenting path for M exists:

def maximum(G):
    """Size of a maximum matching, by trying every choice (small graphs only)."""
    def best(free):
        v = min(free, default=None)
        if v is None:
            return 0
        rest = free - {v}
        return max([best(rest)] + [1 + best(rest - {w}) for w in G[v] & rest])
    return best(frozenset(G))

assert maximum(L) == 6 == len(M) + 1

The difficulty is finding the path.

The augmenting-path search, and where it goes wrong

In a bipartite graph the search grows in layers from an exposed vertex r. Label r even. From an even vertex x, look at each neighbour y. If y is exposed, the path to y is augmenting. If y is unlabelled, label it odd and label its partner even. A vertex keeps the first label it gets. On a bipartite graph that is safe: the side of a vertex decides the parity of every path from r to it, so no vertex could deserve both labels.

Run it on L from s. Then a is odd and b even. Both c and f are neighbours of b, so both become odd, and their partners d and e become even. Now the search is stuck. The edge c–p leaves c, but c is odd, and from an odd vertex the search only follows the matched edge back. From t it fails in the same way: a is labelled odd from w at the third layer, long before the path around the other side could reach a as an even vertex.

s a b c d e f p q t u w
The layered search from s, in any scan order. Green: even (reached by a path of even length that ends in a matched edge). Amber: odd. Vertex c is labelled odd from b, so its edge to p is never followed, and d–e (orange) joins two even vertices, which a search for bipartite graphs never meets.

This failure does not depend on luck. The block below runs the layered search in every order of scanning neighbours (every permutation at every vertex), from both exposed vertices:

from itertools import permutations, product

def layered_search(G, M, r, order):
    """The search from Network Flow and Matchings: the first label wins."""
    mate = {v: w for e in M for v, w in (tuple(e), tuple(e)[::-1])}
    label, queue = {r: "even"}, [r]
    while queue:
        x = queue.pop(0)
        for y in order[x]:
            if y in label:
                continue
            if y not in mate:
                return True
            label[y], label[mate[y]] = "odd", "even"
            queue.append(mate[y])
    return False

names = sorted(L)
orders = [dict(zip(names, pick))
          for pick in product(*(permutations(sorted(L[v])) for v in names))]
assert len(orders) == 6 ** 3 * 2 ** 8          # 55,296 scan orders
assert not any(layered_search(L, M, r, o) for r in "st" for o in orders)

The picture shows what went wrong. The edge d–e joins two even vertices, which never happens in a bipartite graph, and closes the cycle b,c,d,e,f of length 5. Going round it the other way, s,a,b,f,e,d,c reaches c by an even path ending in a matched edge, and c–p–q–t finishes an augmenting path. With odd cycles, "odd" and "even" describe a path, not a vertex, and the first path found is not always the useful one.

Predict: would a depth-first search, which follows one path as far as it goes, avoid the problem?

Only by luck. A depth-first search that happens to try f before c at b walks s,a,b,f,e,d, labels c even, and finds the path. One that tries c first labels c odd and fails. For any augmenting path there is some scan order that follows it, so some order always succeeds; but an algorithm needs a rule that works for every order.

Blossoms, and the idea of shrinking them

Search from all exposed vertices at once. Each exposed vertex is the root of a tree, and the trees together form an alternating forest. An even vertex x scans its neighbours y:

  • y unlabelled: y is matched (every exposed vertex is a root), so label y odd with parent x and its partner even with parent y. The trees grow.
  • y even, in a different tree: the path from the root of x's tree to x, the edge x–y, and the path from y to its root is augmenting. Stop.
  • y even, in the same tree: the edge x–y and the two tree paths up to where they meet form an odd cycle.
  • y odd: nothing to do.

The odd cycle in the third case is a blossom. It has 2k+1 vertices and k matched edges. The vertex where the two tree paths meet is its base. The base is even, and it is the one vertex of the cycle whose matched edge, if it has one, leaves the cycle. The tree path from the root to the base is the stem.

Every vertex of a blossom is reachable from the root by an even alternating path: down the stem to the base, then round the cycle in the direction that starts with an unmatched edge. So every vertex of the blossom deserves to be even. Edmonds' algorithm (1965) makes that true by shrinking the blossom into one new even vertex, adjacent to every neighbour of the cycle. It takes over the base's matched edge and place in the tree, and the k matched edges inside the cycle disappear. The search carries on in the smaller graph G/B, and a path found there is lifted back into G through each shrunk blossom, newest first.

Invariant

At every moment the search holds an alternating forest in the current shrunk graph: every root is exposed and even, every odd vertex has one even child (its partner), and every shrunk vertex is even and stands for an odd set of original vertices.

Edmonds' search on the twelve students

Take the rule "breadth first; roots, and each vertex's neighbours, in alphabetical order". The roots are s and t. From s: a becomes odd and b even. From t: q odd and p even, then u odd and w even. From b: c odd and d even, then f odd and e even. p and w find only odd neighbours. Then d scans e: both even, both in the tree of s. Their tree paths meet at b, so the blossom is b,c,d,e,f with base b.

s a b c d e f p q t u w
Edmonds' forest on L when the blossom is found: trees rooted at s and at t (green even, amber odd; green edges are the edges that labelled an odd vertex, blue the matched edges). The edge d–e (orange) joins two even vertices of the tree of s: it closes the odd cycle b c d e f with base b.

Shrink it to a vertex B. In L/B the matching has three edges: a–B (inherited from b), p–q and u–w. B is even, with parent a, and it is scanned again. Its neighbours are a (odd) and p, which B inherits from c. p is even in the tree of t. Two trees meet, and the path is s,a,B,p,q,t.

s a p q t u w B
L/B: the blossom shrunk to one vertex B, which is even and keeps b's matched edge to a. B inherits c's edge to p, and p is even in the tree of t, so the edge B–p joins two trees: the path s a B p q t (purple) is augmenting in L/B.

To lift it, look at the edge by which the path leaves B without using B's matched edge: here B–p, which came from c–p. The path must enter the blossom at c and travel to the base b along the side that starts with c's matched edge c–d. That side is c,d,e,f,b, four edges, an even number. The other side, c–b directly, would start with an unmatched edge and break the alternation. Then b's matched edge to a continues the path out of the blossom.

s a b c d e f p q t u w
The path lifted back into L: B is replaced by the even-length way round the blossom from c to the base b, which starts with c's matched edge: s a b f e d c p q t, nine edges.
def flip(M, path):
    return M ^ {edge(x, y) for x, y in zip(path, path[1:])}

def augments(G, M, path):
    """A simple path between exposed vertices whose flip is a larger matching."""
    ends_free = not any(path[0] in e or path[-1] in e for e in M)
    new = flip(M, path)
    return (len(set(path)) == len(path) and ends_free
            and is_matching(G, new) and len(new) == len(M) + 1)

blossom = ["b", "c", "d", "e", "f"]
assert all(edge(x, y) in {edge(u, v) for u in L for v in L[u]}
           for x, y in zip(blossom, blossom[1:] + blossom[:1]))
assert sum(edge(x, y) in M for x, y in zip(blossom, blossom[1:] + blossom[:1])) == 2

shrink = lambda v: "B" if v in blossom else v          # L/B, as a set of edges
LB_edges = {edge(shrink(x), shrink(y)) for x in L for y in L[x]} - {edge("B", "B")}
LB = {}
for x, y in map(tuple, LB_edges):
    LB.setdefault(x, set()).add(y)
    LB.setdefault(y, set()).add(x)
MB = {edge(shrink(x), shrink(y)) for x, y in map(tuple, M)} - {edge("B", "B")}
assert sorted(LB["B"]) == ["a", "p"] and len(LB) == 8
assert augments(LB, MB, ["s", "a", "B", "p", "q", "t"])

lifted = ["s", "a", "b", "f", "e", "d", "c", "p", "q", "t"]
assert augments(L, M, lifted)
assert not augments(L, M, ["s", "a", "b", "c", "p", "q", "t"])   # the short side
assert len(flip(M, lifted)) == 6                                 # everyone has a room

Why shrinking is safe

Say the search has found a blossom B with base β in the graph G with matching M, and G′=G/B, M′=M/B are the shrunk graph and matching.

Lemma (lifting). If P′ is an augmenting path for M′ in G′, then G has an augmenting path for M, obtained by replacing the shrunk vertex b by a path through B.

Proof. If b is not on P′, P′ is augmenting in G as it stands: its edges and their matched status are unchanged. Otherwise, write the cycle as β=c0,c1,…,c2k with c1c2,c3c4,…,c2k−1c2k matched. The vertex b has at most one matched edge in G′, the base's old edge. So among its edges on P′, one is unmatched, u–b, and the other, if b is not an end of P′, is matched and leads to the base's partner. Choose a ci adjacent to u in G. If i=0, use β alone. If i is odd, go ci,ci+1,…,c2k,β. If i>0 is even, go ci,ci−1,…,c1,β. Either way the segment has even length, starts at ci with a matched edge and arrives at β on an unmatched one. So u–ci (unmatched), the segment, and then β's matched edge out of B alternate. If instead b was an end of P′, then β is exposed and the segment ends the path there. The segment stays inside B and the rest of P′ outside it, so the path is simple. ◻

The converse (an augmenting path in G gives one in G′) also holds for a blossom found by the search. Sketch, not proved here: flipping the stem, which has even length, keeps |M| and makes β exposed. Then take an augmenting path in G, start at an end that is not β and stop at the first vertex of B: shrunk, that prefix ends at the now exposed b. The algorithm below does not need the converse, because it proves that its final matching is maximum in another way: with a certificate.

The certificate: Tutte–Berge

Suppose c and p fall out and no longer agree to share. The algorithm stops at five rooms. Why can nobody do better?

Remove a set U of vertices and count the connected pieces of G−U that have an odd number of vertices: odd(G−U). Inside an odd piece the vertices can't all be matched to each other, so each odd piece has a vertex that is exposed or matched to a vertex of U, and U can absorb at most |U| of them.

Theorem (Tutte–Berge formula)

For every graph G, the size of a maximum matching is ν(G)=minU⊆V12(|V|+|U|−odd(G−U)).

The easy half, proved. For every matching M and every U, at least odd(G−U)−|U| vertices are exposed, so |M|≤12(|V|−odd(G−U)+|U|). This is the analogue of weak duality in Network Flow and Matchings: every U is an upper bound, and a matching that meets one is maximum.

The other half, witnessed by the algorithm. When a search from all exposed vertices ends with no path, let U be its odd vertices. They are original vertices, since shrunk vertices are always even. When the search stops, every edge from an even vertex has been scanned, so every neighbour of an even vertex is odd or lies inside the same shrunk vertex. So each even vertex, expanded back into the original vertices it stands for, is a whole connected piece of G−U, and it is odd: a blossom is an odd cycle of pieces that are each odd. The unlabelled vertices are matched to each other and have no even neighbours, so their pieces have even size. In each tree every odd vertex has exactly one even child, and every even vertex except the root is the child of one odd vertex, so there are |U|+(number of roots) even vertices. Therefore odd(G−U)=|U|+(exposed vertices), and the bound becomes 12(|V|−exposed)=|M|. The matching meets the bound, so it is maximum. With the lifting lemma, that proves the algorithm correct, and the minimum in the formula is attained.

s a b c d e f p q t u w
L without the edge c–p, at the end of the algorithm: a maximum matching of 5 (blue) and the final forest, rooted at the unmatched s and w. The odd vertices a, q and u (red rings) form U. Removing them leaves five odd groups: the blossom {b, c, d, e, f}, {s}, {w}, {t} and {p}.
L2 = {x: set(ys) for x, ys in L.items()}      # c and p no longer share
L2["c"].discard("p")
L2["p"].discard("c")

def odd_pieces(G, U):
    seen, odd = set(U), 0
    for v in G:
        if v not in seen:
            stack, size = [v], 0
            seen.add(v)
            while stack:
                x = stack.pop()
                size += 1
                for y in G[x] - seen:
                    seen.add(y)
                    stack.append(y)
            odd += size % 2
    return odd

def bound(G, U):
    return (len(G) + len(U) - odd_pieces(G, U)) // 2

U = {"a", "q", "u"}
assert odd_pieces(L2, U) == 5 and bound(L2, U) == 5 == maximum(L2)
subsets = product([False, True], repeat=12)
assert min(bound(L2, {v for v, inside in zip(names, pick) if inside})
           for pick in subsets) == 5                   # no set proves less

The notice for the two students left over writes itself: "Without a, q and u the rest split into five groups of odd size. Each group has someone who must room with a, q or u or not at all, so at least two students are left without a room."

Tutte's theorem (1947) is the case of a perfect matching: G has one if and only if odd(G−U)≤|U| for every U. It follows from the formula with ν(G)=|V|/2.

Cost

Count in the word-RAM model with the graph stored as adjacency lists and per-vertex arrays for labels, parents and partners. A scan is one look at a neighbour of a vertex taken from the queue.

Between two shrinks a search labels each vertex at most once, and each shrink removes at least two vertices (a blossom has at least three), so there are at most V/2 shrinks per search. Each contraction can require O(V+E) work to rebuild the shrunk graph and rescan what changed, and between two contractions the search scans each vertex's neighbours at most once, another O(V+E). With O(V) contractions in one search, one search therefore costs O(V·(V+E)) in the worst case. Each search except the last adds an edge to the matching, so there are at most ν+1≤V/2+1 searches. That gives a worst-case total of

O(V2(V+E)),

which is O(V2E) when no vertex is isolated. Careful implementations reach O(V3) by shrinking without rebuilding (Gabow, 1976). Micali and Vazirani (1980) reach O(EV), the Hopcroft–Karp bound for bipartite matching. A complete and correct proof of their algorithm was finally published in journal form in 2024 (Vazirani, Math. Oper. Res.). Both are cited here, not proved.

Maximum-weight matching in general graphs is also solvable in polynomial time (Edmonds, 1965, with blossoms and LP duality), but it is outside this module.

Measure the claim

Here are the searches on random graphs where each of n vertices picks 3 random others (seed 451+n), starting from the empty matching and scanning in increasing order:

n E ν searches blossoms shrunk scans scans / ((ν+1)·2E)
100 290 50 51 10 274 0.009
200 592 100 101 120 855 0.007
400 1,193 200 201 37 2,166 0.005

Each graph has a perfect matching, and there are exactly ν+1 searches. Scans stay far below the bound because early searches find an exposed neighbour in a step or two, and the number of blossoms jumps with the instance. These are measurements on these instances, not guarantees; the lab builds a family where a single search shrinks about V/4 blossoms.

A problem that looks different

A build farm has two identical machines and a batch of one-minute jobs. Some jobs must finish before others start. In each minute, a machine runs one job, or sits idle. Two jobs can share a minute only if neither has to come before the other. What is the fewest number of minutes, and why might that number depend on how many jobs can be run two at a time? Nothing here has two sides. The lab's last problem is a different one.

Practise

In the lab you flip augmenting paths and reject paths that only look like them; predict, then trace, the forest, the blossom and the shrunk graph on a new instance; write the whole blossom algorithm with its certificate and test it against brute force and against the search from module 11; count the scans on graphs where the number of blossoms per search grows with the graph; and solve a pairing problem that doesn't say what it is.

Recap

You can now: prove Berge's theorem; explain why the layered search of bipartite matching misses augmenting paths through odd cycles; find, shrink and lift a blossom; and read a Tutte–Berge set U off the final forest, as a certificate that anyone can check.

Invariant: the search keeps an alternating forest whose roots are exactly the exposed vertices, and every shrunk vertex is even and stands for an odd set of original vertices. When no path is left, the odd vertices U satisfy |M|=12(|V|+|U|−odd(G−U)).

Complexity achieved: at most ν+1 searches of O(V(V+E)) each: O(V2(V+E)) worst case, polynomial, against trying every subset of edges. Faster: O(V3) (Gabow) and O(EV) (Micali–Vazirani), cited.

Failure mode: treating the graph as bipartite. The search keeps a vertex's first label, and a vertex that should be even through an odd cycle stays odd, so the search stops at a matching that is not maximum and reports nothing wrong.

In real software: NetworkX's max_weight_matching(G, maxcardinality=True) computes a maximum matching with Edmonds' blossom method, and its documentation states O(V3) time. The Boost Graph Library's checked_edmonds_maximum_cardinality_matching runs Edmonds' algorithm and then verifies the result by checking the Tutte–Berge equality with U taken from the search's odd labels.

Retrieval (Network Flow and Matchings): in bipartite matching, the certificate that no larger matching exists was a minimum cut. How does a minimum cut in the unit network give a set of at most ν people and jobs that touches every allowed pair?

Check yourself

  1. Why is the set of odd vertices of the final forest a Tutte–Berge set that meets the bound? Count the even vertices.
  2. Remove the edge w–a from L and keep the same five rooms. Does the layered search still fail from both s and t? What was that edge doing?
  3. On a bipartite graph, compare the blossom algorithm with the search from Network Flow and Matchings. What does the blossom algorithm do there that the other does not?

Topic list modeled on the public schedule of CMU 15-451/651 Algorithm Design and Analysis (Fall 2025). All lecture text, proofs, examples, code, and exercises are original to SciMigo; no CMU course materials are reproduced. This course is independent and not affiliated with or endorsed by Carnegie Mellon University.

Your turn

Now try it yourself

Write the code, press Run, and scrub through the frames your program draws. The checks tell you when it works.

Loading the lab…

In this lab

  1. Flip an augmenting path, and reject the ones that only look like one

    The whole module rests on one move from Network Flow and Matchings, now in a graph with no sides: flip an augmenting path. Write it, and write the test that decides whether a path really is one.

    A graph is a dict {vertex: set of neighbours}. A matching is a dict mate: mate[v] is v's partner, and a vertex that is not a key of mate is exposed.

    First, why_not(G, mate, path). It returns None when path is augmenting: a simple path along edges of G, between two exposed vertices, whose edges are in turn outside and inside the matching. Otherwise it returns the first of these reasons that applies, tested in this order: "too short" (no edge at all), "repeats a vertex", "not an edge", "end not exposed", "does not alternate". Second, augment(mate, path): flip the path in place, so that its edges outside the matching come in and those inside go out.

    The program flips four paths on an 8-vertex graph with a 5-cycle, drawing a frame before each flip (the path in purple, matched edges in blue) and one after the last. Then it asks why_not about five paths against the matching after the second flip. Before you press Run: the third path goes all the way round the 5-cycle, and the fourth path uses the edge 4–0. Who is 0 matched to at the end, and why could the fourth path not have used 3–4 instead?

  2. Predict the forest, then find and shrink the blossom

    Now the search the lesson traced, on a new graph T: twelve vertices, five matched edges, and the exposed vertices 0 and 11. The search grows a tree from each exposed vertex, breadth first, each vertex scanning its neighbours in increasing order. The loop is given; it draws one frame per event, with even vertices green, odd vertices amber, matched edges blue and the blossom purple.

    You write the two pieces that make it Edmonds' search. blossom_cycle(parent, x, y) is called when x scans y and both are even in the same tree. parent[v] is v's tree parent (None at a root). Return the odd cycle closed by the edge x–y as a list that starts at its base, the lowest vertex the two tree paths share, and then goes once round the cycle, in either direction. contract(G, mate, cycle, b) shrinks the cycle into one new vertex b and returns new dicts (G2, mate2): b is adjacent to every vertex outside the cycle that touched it, b takes over the base's partner (if the base had one), and the matched edges inside the cycle disappear. Don't change G or mate.

    Answer the three questions before you press Run; they ask what the forest does at each step. The frames then show whether you were right.

  3. The blossom algorithm, with its certificate

    Now the whole algorithm. max_matching(G) returns (mate, U): a maximum matching, and a certificate U, a list of vertices with len(matching) == bound(G, U), where bound(G, U) is the lesson's Tutte–Berge bound (|V| + |U| − number of odd pieces of G − U) / 2. The bound holds for every U and every matching, so meeting it proves that no larger matching exists. bound is given.

    The starter is the search from Network Flow and Matchings: from each exposed vertex in turn, first label wins, and an edge between two even vertices is ignored. It returns no certificate, and on graphs with odd cycles it stops too early. Replace find_path with Edmonds' search: grow alternating trees from all exposed vertices at once; when two even vertices of different trees meet, return the augmenting path; when two even vertices of the same tree meet, shrink the blossom and carry on; and lift the path back through every shrunk blossom, newest first. When the search runs dry, its odd vertices are U. You may reuse your exercise 2 code.

    The demo draws the answer on a hub joined to two triangles and a 5-cycle, with U ringed in red. The checks compare you with brute force on small graphs and with your own certificate on larger ones, including chains of blossoms nested inside each other. Don't change G.

  4. Count the searches, the scans and the blossoms

    The lesson proved that the blossom algorithm makes exactly ν + 1 searches (ν = the size of a maximum matching), each costing O(V(V + E)). Exercise 3's algorithm keeps every neighbourhood as a set, so its shrunk graph never holds a repeated edge; then one search scans at most 2E neighbours of original vertices (each is taken from the queue at most once, and shrinking never adds to its neighbours) plus at most V for each blossom it shrinks. Measure both, and find out how far below the O(V²(V + E)) bound real runs stay.

    Matcher is exercise 3's algorithm with counters. Write three functions. sparse(n, seed) builds a graph on 0..n − 1: with rng = random.Random(seed), each vertex v in turn joins every w in rng.sample(range(n), 3) except itself. chain(k) builds vertex 0 followed by k triangles in a row, as its docstring says. profile(G) runs one Matcher(G) and returns a dict with "V", "E", "nu", "searches", "shrinks", "scans", "last_shrinks" (blossoms shrunk by the final search, the one that finds nothing) and "worst", the largest scans ÷ (2E + V · shrinks) over the searches.

    The program profiles sparse graphs of 100, 200 and 400 vertices and chains of 101, 201 and 401 vertices, and draws two of your numbers as bars. The checks test them against the proofs, then run profile on graphs of their own and compare each number with what its Matcher counted. Answer the two questions first: the first asks how many blossoms the last search on a chain shrinks.

  5. Mastery challenge: rope teams for the climb-a-thon

    The Granite Hall climbing gym runs a charity climb-a-thon every Saturday. Every route is climbed by a rope team of two: one climbs, the other belays, then they swap. Two members can form a team only if they are certified on the same rope system and their weights are within the gym's limit, and the front desk has a survey listing every compatible pair. Up to 600 members sign up, with a few thousand compatible pairs.

    Write teams(n, compatible). Members are numbered 0 to n − 1 and compatible is a list of pairs (a, b). Return (pairs, key): as many teams as possible, each a compatible pair, nobody in two teams. If everybody is in a team, key is None. If anyone sits out, the members who sit out will ask why, so key must be a list of members that proves no plan does better: take the key members out, and the rest fall into groups linked by compatible pairs; each group with an odd number of members has someone who must team up with a key member or sit out. The given most_teams_allowed(n, compatible, key) computes the limit that follows, and yours must equal len(pairs).

    The starter is correct but tries every arrangement and then every key. The survey is kept by the front desk, and every pair your code reads from compatible counts. A check runs a group of 600 members twice, once when everybody can climb and once when some can't, and allows 20 reads per pair; the starter runs out long before it finishes. (The count is feedback, not security: a copy of the survey is charged once and free after that.)

    Answer the four questions, then write it. The demo draws Saturday's group of 11: teams in blue, members sitting out in pink, and your key ringed in red.