A matching is a set of edges without common endpoints. Everything rests on Berge’s theorem: a matching is maximum iff it has no augmenting path (alternating path between two free vertices). In bipartite graphs augmenting paths are found by plain DFS/BFS, and the min-max theorems of König, Hall and Dilworth make matching a modelling tool for covers, independent sets and posets.

AlgorithmTimeNotes
Kuhn (DFS augmenting paths), fast in practicerounds with a shared visited array + greedy look-ahead
Hopcroft–Karpphases of disjoint shortest augmenting paths
Dinic on the unit networkreuse the flow template
weightedHungarian / min-cost flowAssignment Problem

Kuhn

struct Kuhn {
    int n, m, it = 0; vector<vector<int>> g; vector<int> ml, mr, vis;
    Kuhn(int n, int m) : n(n), m(m), g(n), ml(n, -1), mr(m, -1), vis(n) {}
    void addEdge(int u, int v) { g[u].push_back(v); }
    bool dfs(int u) {
        vis[u] = it;
        for (int v : g[u]) if (mr[v] < 0) { ml[u] = v, mr[v] = u; return true; }   // look-ahead: free neighbour
        for (int v : g[u]) if (vis[mr[v]] != it && dfs(mr[v])) { ml[u] = v, mr[v] = u; return true; }
        return false;
    }
    int run() {
        int res = 0;
        for (bool ch = true; ch;) { ch = false; it++; for (int u = 0; u < n; u++) if (ml[u] < 0 && dfs(u)) res++, ch = true; }
        return res;
    }
};

The visited array is shared by a whole round; rounds repeat until one finds nothing. Shuffle adjacency lists against adversarial tests, and seed the matching with a cheap greedy pass first (match any left vertex to any free neighbour before the main loop) — this alone typically removes 70–90% of the augmenting searches.

Hopcroft–Karp

struct HopcroftKarp {
    int n, m, lim; vector<vector<int>> g; vector<int> ml, mr, dist, it;
    HopcroftKarp(int n, int m) : n(n), m(m), g(n), ml(n, -1), mr(m, -1), dist(n), it(n) {}
    void addEdge(int u, int v) { g[u].push_back(v); }
    bool bfs() {
        queue<int> q; lim = INT_MAX;
        for (int u = 0; u < n; u++) if (ml[u] < 0) dist[u] = 0, q.push(u); else dist[u] = INT_MAX;
        while (sz(q)) {
            int u = q.front(); q.pop();
            if (dist[u] >= lim) continue;
            for (int v : g[u]) { int w = mr[v]; if (w < 0) lim = min(lim, dist[u]); else if (dist[w] == INT_MAX) dist[w] = dist[u] + 1, q.push(w); }
        }
        return lim != INT_MAX;
    }
    bool dfs(int u) {
        for (int& i = it[u]; i < sz(g[u]); i++) {
            int v = g[u][i], w = mr[v];
            if (w < 0 ? dist[u] == lim : dist[w] == dist[u] + 1 && dfs(w)) { ml[u] = v, mr[v] = u; return true; }
        }
        return false;
    }
    int run() {
        int res = 0;
        while (bfs()) { fill(all(it), 0); for (int u = 0; u < n; u++) if (ml[u] < 0 && dfs(u)) res++; }
        return res;
    }
};

Berge’s theorem holds verbatim for general (non-bipartite) graphs too — the matching theory above is specifically about finding the augmenting path by plain DFS/BFS, which relies on bipartiteness; on general graphs an odd cycle can hide the path and needs blossom contraction instead (General Matching). Online bipartite matching (right vertices, or edges, arrive over time and must be matched irrevocably) is a different problem entirely — greedy achieves only a ratio, and the optimal randomized ranking algorithm achieves .

König, Hall and the covering formulas

König: in a bipartite graph, max matching = min vertex cover. From a maximum matching, let be the vertices reachable from free left vertices by alternating paths; the cover is .

QuantityValue
minimum vertex cover (max matching)
maximum independent set (complement of a cover)
minimum edge cover (no isolated vertices) (Gallai): matching + one edge per free vertex
maximum matching (König–Ore deficiency)$
pair<vector<int>, vector<int>> konigCover(int n, int m, const vector<vector<int>>& g, const vector<int>& ml, const vector<int>& mr) {
    vector<char> zl(n), zr(m); vector<int> st, L, R;              // input: a maximum matching
    for (int u = 0; u < n; u++) if (ml[u] < 0) zl[u] = 1, st.push_back(u);
    while (sz(st)) {
        int u = st.back(); st.pop_back();
        for (int v : g[u]) if (!zr[v]) { zr[v] = 1; int w = mr[v]; if (w >= 0 && !zl[w]) zl[w] = 1, st.push_back(w); }
    }
    for (int u = 0; u < n; u++) if (!zl[u]) L.push_back(u);
    for (int v = 0; v < m; v++) if (zr[v]) R.push_back(v);
    return {L, R};                                                // |L| + |R| = matching size
}

Hall: can be fully matched iff for all . Corollaries: every -regular bipartite graph () has a perfect matching, hence its edges split into perfect matchings; a “for every subset, enough neighbours” condition in a constructive problem means a matching exists.

Which vertices and edges are forced

With a maximum matching , orient unmatched edges and matched edges . Let = vertices reachable from free left vertices, = vertices reachable from free right vertices in the reversed orientation.

  • A left vertex is matched in every maximum matching iff it is not in (symmetrically on the right with ).
  • An edge is in some maximum matching iff , or , or and lie in the same SCC of the oriented graph (an even alternating cycle). Edges of are trivially in some.
  • An edge is in every maximum matching iff , , and are in different SCCs.
struct MatchingClassifier {                                       // after a maximum matching (ml, mr)
    vector<char> zl, zr; vector<int> comp;                       // comp over n + m vertices (right v -> n + v)
    MatchingClassifier(int n, int m, const vector<vector<int>>& g, const vector<int>& ml, const vector<int>& mr) {
        vector<vector<int>> gr(m); for (int u = 0; u < n; u++) for (int v : g[u]) gr[v].push_back(u);
        zl.assign(n, 0); zr.assign(m, 0); vector<int> st;
        for (int u = 0; u < n; u++) if (ml[u] < 0) zl[u] = 1, st.push_back(u);
        while (sz(st)) { int u = st.back(); st.pop_back(); for (int v : g[u]) { int w = mr[v]; if (w >= 0 && !zl[w]) zl[w] = 1, st.push_back(w); } }
        for (int v = 0; v < m; v++) if (mr[v] < 0) zr[v] = 1, st.push_back(v);
        while (sz(st)) { int v = st.back(); st.pop_back(); for (int u : gr[v]) { int w = ml[u]; if (w >= 0 && !zr[w]) zr[w] = 1, st.push_back(w); } }
        int N = n + m, timer = 0, nc = 0; comp.assign(N, -1); vector<int> num(N, -1), low(N), stk;
        auto dfs = [&](auto& self, int x) -> void {
            num[x] = low[x] = timer++; stk.push_back(x);
            auto visit = [&](int y) { if (num[y] < 0) self(self, y), low[x] = min(low[x], low[y]); else if (comp[y] < 0) low[x] = min(low[x], num[y]); };
            if (x < n) { for (int v : g[x]) if (ml[x] != v) visit(n + v); }  // unmatched L -> R
            else if (mr[x - n] >= 0) visit(mr[x - n]);                       // matched R -> L
            if (low[x] == num[x]) { int y; do { y = stk.back(); stk.pop_back(); comp[y] = nc; } while (y != x); nc++; }
        };
        for (int x = 0; x < N; x++) if (num[x] < 0) dfs(dfs, x);
    }
    bool leftAvoidable(int u) const { return zl[u]; }
    bool rightAvoidable(int v) const { return zr[v]; }
    bool edgeInSome(int u, int v, const vector<int>& ml) const { return ml[u] == v || zl[u] || zr[v] || comp[u] == comp[sz(zl) + v]; }
    bool edgeInEvery(int u, int v, const vector<int>& ml) const { return ml[u] == v && !zl[u] && !zr[v] && comp[u] != comp[sz(zl) + v]; }
};

Posets: Dilworth and Mirsky

  • Dilworth: the minimum number of chains covering a poset equals the maximum antichain. Compute: transitive closure, split graph ( for ), chains ; the antichain is for a König cover .
  • Mirsky (dual): the minimum number of antichains covering a poset equals the longest chain (group by the length of the longest chain ending at each element). For sequences this is “minimum number of non-increasing subsequences = LIS” (patience sorting).
  • Disjoint paths vs chains: vertex-disjoint path cover of a DAG uses the split graph without closure (Flow Modeling).
vector<int> maxAntichain(int n, const vector<vector<char>>& less) {   // less = transitive relation, less[x][y]: x < y
    HopcroftKarp hk(n, n);
    for (int x = 0; x < n; x++) for (int y = 0; y < n; y++) if (less[x][y]) hk.addEdge(x, y);
    hk.run();
    auto [cl, cr] = konigCover(n, n, hk.g, hk.ml, hk.mr);
    vector<char> bad(n); for (int x : cl) bad[x] = 1; for (int y : cr) bad[y] = 1;
    vector<int> res; for (int x = 0; x < n; x++) if (!bad[x]) res.push_back(x);
    return res;                                                     // size = n - matching = min chain cover
}

Greedy special cases

  • Convex bipartite graphs (each left vertex is adjacent to an interval of ordered right vertices; e.g. jobs and time slots): sweep the right vertices in order and give each one to the available left vertex with the smallest (a heap). , optimal.
  • Unit-interval / points on a line matchings are usually this greedy or a sorted DP.
int convexMatching(int m, vector<pair<int, int>> iv) {             // left i adjacent to right [l, r] in 0..m-1
    sort(all(iv)); priority_queue<int, vector<int>, greater<>> pq; int res = 0, k = 0;
    for (int j = 0; j < m; j++) {
        while (k < sz(iv) && iv[k].first == j) pq.push(iv[k++].second);
        while (sz(pq) && pq.top() < j) pq.pop();
        if (sz(pq)) pq.pop(), res++;
    }
    return res;
}

Modelling

  • Rows and columns: placing non-attacking rooks on allowed cells = matching rows to columns; covering all marked cells with fewest lines = König cover.
  • Grid graphs are bipartite (chessboard colouring): domino tilings, maximum independent set of cells, knights on a board.
  • Perfect matching in general bipartite graphs with on one side: count with DP over subsets; the number of perfect matchings is a permanent (#P-hard in general).
  • Bottleneck / threshold: binary search a parameter and test for a perfect matching.
  • Vertex capacities (-matching): flow with capacities on source and sink edges.
  • Preference-based pairing (“everyone ranks the other side”) is a different problem, not a maximum/min-cost matching — see Stable Matching (Gale–Shapley).