Back to Graph Theory Series

Hopcroft-Karp Algorithm

August 30, 2026 Wasil Zafar 28 min read

Why find one augmenting path at a time when you could find dozens simultaneously? This 1973 algorithm processes bipartite matching in "phases," and proves there can never be more than about √V of them.

Contents

  1. Matching & Augmenting Paths
  2. Why Flipping Works
  3. BFS Layers + DFS Batch
  4. Worked Phase
  5. Correctness & Phase Bound
  6. Implementation
  7. Complexity & Alternatives
  8. Applications
  9. Pitfalls & Checklist
  10. Exercises
  11. Historical Note

Matching and Augmenting Paths

Let $G=(L\cup R,E)$ be bipartite: every edge joins a left vertex to a right vertex. A matching $M$ is a set of edges with no shared endpoint. Its size $|M|$ counts how many left–right pairs have been formed, and a maximum matching has the largest possible size.

Matched edge

An edge currently chosen in $M$. Each vertex touches at most one such edge.

Free vertex

A vertex with no current partner. Augmenting paths begin and end free.

Alternating path

A path whose edges alternate between outside and inside the matching.

An augmenting path starts at a free left vertex, ends at a free right vertex, and alternates unmatched–matched–unmatched edges. It contains one more unmatched edge than matched edge. Flipping every edge along the path therefore increases the matching size by exactly one.

$$M' = M\triangle P\qquad\Longrightarrow\qquad |M'|=|M|+1,$$

where $\triangle$ is symmetric difference: remove path edges already in $M$ and add path edges not in $M$.

Intuition: A Chain of Reassignments

A free applicant wants an occupied role. The current holder can move to another eligible free role, perhaps after several handoffs. Alternation guarantees that every displaced person receives a new assignment, while the initially free applicant becomes matched. Nobody ends with two roles.

Why Flipping an Augmenting Path Works

An augmenting path before and after its edges are flipped Before augmentation, A is matched to right vertex 1 while C and 3 are free. The augmenting path C-1-A-3 alternates unmatched, matched, unmatched. After flipping, C is matched to 1 and A to 3, increasing the matching size by one. Before: |M| = 1 LeftRight A C 1 3 Path: C — 1 — A — 3 flip After: |M| = 2 LeftRight A C 1 3 C—1 and A—3 are now matched current match augmenting path new match
The flip preserves the matching rule at every internal vertex: one matched path edge is removed and the other is added. Only the two free endpoints gain matched edges, so cardinality rises by one.

Berge's Theorem Is the Finish Line

A matching is maximum if and only if no augmenting path exists. Every successful phase grows the matching; a BFS that cannot reach any free right vertex certifies optimality.

One Phase: BFS Layers, Then a DFS Batch

A basic augmenting-path algorithm searches, flips one path, and starts over. Hopcroft–Karp amortizes the search by handling a whole maximal set of vertex-disjoint shortest augmenting paths in one phase.

  1. Multi-source BFS: begin from every free left vertex at distance zero.
  2. Build the alternating layered graph: move left-to-right only through unmatched edges, then right-to-left only through the right vertex's current matched edge.
  3. Stop at the shortest free-right layer: once a free right vertex is reachable, record that augmenting distance and do not build deeper layers.
  4. Layer-respecting DFS: from each free left vertex, follow only edges that advance exactly one BFS layer and finish at that shortest distance.
  5. Flip all found paths: the DFS paths are vertex-disjoint, so their updates do not conflict.

What BFS Actually Measures

Most implementations store distances only for left vertices. Crossing an unmatched edge to right vertex $v$ and then its matched edge to left vertex pairV[v] advances one left-layer. A separate distNil records the first level that reaches any free right vertex.

Worked Phase: Two Paths at Once

Suppose the current matching is $M=\{A\!-!1, B\!-!2\}$. Left vertices $C,D$ and right vertices $3,4$ are free. The graph contains the alternating routes $C-1-A-3$ and $D-2-B-4$.

Hopcroft–Karp BFS layers with two disjoint shortest augmenting paths Four layers show free left vertices C and D, right vertices 1 and 2, their matched left partners A and B, and free right vertices 3 and 4. Two disjoint length-three augmenting paths can be flipped in one phase. Layer 0 · free L Layer 1 · R Layer 2 · matched L Layer 3 · free R C D 1 2 A B 3 4 0 1 0 1 DFS extracts C—1—A—3 and D—2—B—4; matching size 2 → 4
Dashed crimson edges are unmatched moves from left to right; thick navy edges are current matches followed back to the left. Both shortest paths have three edges and share no vertex, so the phase augments twice.
StageStateMeaning
BFS seed$C,D$ at left-distance $0$Both unmatched left vertices start simultaneously.
First alternating step$C\to1\to A$ and $D\to2\to B$Unmatched edge out, matched edge back.
Free right reached$A\to3$ and $B\to4$The shortest augmenting length is three edges.
DFS batchTwo vertex-disjoint pathsThey form a maximal set in this layered graph.
Flip$M'=\{C\!-!1,A\!-!3,D\!-!2,B\!-!4\}$Matching size rises from two to four.

Correctness and the $O(\sqrt{V})$ Phase Bound

Three facts connect the phase mechanics to the final guarantee:

  1. Every DFS success is a valid augmentation. Layer rules enforce alternation, and vertex-disjoint paths can be flipped together without assigning any vertex twice.
  2. The shortest augmenting-path length strictly increases after a phase. DFS finds a maximal set that blocks every shortest route in the current layered graph. Any surviving augmenting path must therefore be longer.
  3. No augmenting path means maximum. This is exactly Berge's theorem.

The phase count uses a short-path/long-path split. During the first $O(\sqrt{V})$ phases, the shortest augmenting length increases at least once per phase. Once that length exceeds $\sqrt{V}$, compare the current matching $M$ with a maximum matching $M^*$. Their symmetric difference decomposes into vertex-disjoint alternating cycles and paths. Every remaining augmenting component is long, so only $O(\sqrt{V})$ such components can fit among $V$ vertices. Hence the deficit $|M^*|-|M|$ is $O(\sqrt{V})$, and at most that many additional phases are possible.

Why “Maximal” Is Enough

The DFS batch need not contain the greatest possible number of disjoint shortest paths. It only needs to be maximal: no additional shortest path can be added without sharing a vertex. That blocking property eliminates every shortest augmenting route and forces the next phase deeper.

Implementation

The code below numbers left vertices $0\ldots |L|-1$ and right vertices $0\ldots |R|-1$. pairU[u] is the right partner of left vertex $u$; pairV[v] is the left partner of right vertex $v$; $-1$ means free. The explicit distNil counts unmatched left-to-right steps on a shortest augmenting route, so its ordinary edge length is $2\cdot\text{distNil}-1$.

from collections import deque

def hopcroft_karp(adj, right_size):
    """adj[u] lists the right-side neighbors of left vertex u."""
    left_size = len(adj)
    FREE = -1
    INF = left_size + 1
    pair_u = [FREE] * left_size
    pair_v = [FREE] * right_size
    dist = [INF] * left_size
    dist_nil = INF

    def bfs():
        nonlocal dist_nil
        queue = deque()
        for u in range(left_size):
            if pair_u[u] == FREE:
                dist[u] = 0
                queue.append(u)
            else:
                dist[u] = INF

        dist_nil = INF
        while queue:
            u = queue.popleft()
            if dist[u] >= dist_nil:
                continue  # never build deeper than a shortest augmenting path

            for v in adj[u]:
                mate = pair_v[v]
                if mate == FREE:
                    dist_nil = dist[u] + 1
                elif dist[mate] == INF:
                    dist[mate] = dist[u] + 1
                    queue.append(mate)

        return dist_nil != INF

    def dfs(u):
        for v in adj[u]:
            mate = pair_v[v]
            if mate == FREE:
                if dist[u] + 1 != dist_nil:
                    continue
            elif dist[mate] != dist[u] + 1 or not dfs(mate):
                continue

            pair_u[u] = v
            pair_v[v] = u
            return True

        dist[u] = INF  # prune this dead end for the rest of the phase
        return False

    matching_size = 0
    while bfs():
        for u in range(left_size):
            if pair_u[u] == FREE and dfs(u):
                matching_size += 1

    return pair_u, pair_v, matching_size

# A:{1,3}, B:{2,4}, C:{1}, D:{2}; indices are zero-based.
adj = [[0, 2], [1, 3], [0], [1]]
pair_u, pair_v, size = hopcroft_karp(adj, right_size=4)
print("left partners:", pair_u)  # [2, 3, 0, 1]
print("matching size:", size)    # 4
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

class HopcroftKarp {
    static constexpr int FREE = -1;
    const vector<vector<int>>& adj;
    int leftSize, rightSize, inf, distNil;
    vector<int> pairU, pairV, dist;

    bool bfs() {
        queue<int> ready;
        for (int u = 0; u < leftSize; ++u) {
            if (pairU[u] == FREE) {
                dist[u] = 0;
                ready.push(u);
            } else {
                dist[u] = inf;
            }
        }

        distNil = inf;
        while (!ready.empty()) {
            int u = ready.front();
            ready.pop();
            if (dist[u] >= distNil) continue;

            for (int v : adj[u]) {
                int mate = pairV[v];
                if (mate == FREE) {
                    distNil = dist[u] + 1;
                } else if (dist[mate] == inf) {
                    dist[mate] = dist[u] + 1;
                    ready.push(mate);
                }
            }
        }
        return distNil != inf;
    }

    bool dfs(int u) {
        for (int v : adj[u]) {
            int mate = pairV[v];
            if (mate == FREE) {
                if (dist[u] + 1 != distNil) continue;
            } else if (dist[mate] != dist[u] + 1 || !dfs(mate)) {
                continue;
            }

            pairU[u] = v;
            pairV[v] = u;
            return true;
        }

        dist[u] = inf;
        return false;
    }

public:
    HopcroftKarp(const vector<vector<int>>& graph, int rightCount)
        : adj(graph), leftSize(static_cast<int>(graph.size())),
          rightSize(rightCount), inf(leftSize + 1), distNil(inf),
          pairU(leftSize, FREE), pairV(rightSize, FREE), dist(leftSize, inf) {}

    int maximumMatching() {
        int matching = 0;
        while (bfs()) {
            for (int u = 0; u < leftSize; ++u)
                if (pairU[u] == FREE && dfs(u)) ++matching;
        }
        return matching;
    }

    const vector<int>& leftPartners() const { return pairU; }
};

int main() {
    vector<vector<int>> adj = {{0, 2}, {1, 3}, {0}, {1}};
    HopcroftKarp matcher(adj, 4);
    cout << "Matching size: " << matcher.maximumMatching() << '\n';
    cout << "Left partners: ";
    for (int v : matcher.leftPartners()) cout << v << ' ';
    cout << '\n';
}
import java.util.*;

class HopcroftKarp {
    private static final int FREE = -1;
    private final List<List<Integer>> adj;
    private final int leftSize, inf;
    private int distNil;
    private final int[] pairU, pairV, dist;

    HopcroftKarp(List<List<Integer>> graph, int rightSize) {
        adj = graph;
        leftSize = graph.size();
        inf = leftSize + 1;
        distNil = inf;
        pairU = new int[leftSize];
        pairV = new int[rightSize];
        dist = new int[leftSize];
        Arrays.fill(pairU, FREE);
        Arrays.fill(pairV, FREE);
    }

    private boolean bfs() {
        Deque<Integer> ready = new ArrayDeque<>();
        for (int u = 0; u < leftSize; ++u) {
            if (pairU[u] == FREE) {
                dist[u] = 0;
                ready.add(u);
            } else {
                dist[u] = inf;
            }
        }

        distNil = inf;
        while (!ready.isEmpty()) {
            int u = ready.remove();
            if (dist[u] >= distNil) continue;

            for (int v : adj.get(u)) {
                int mate = pairV[v];
                if (mate == FREE) {
                    distNil = dist[u] + 1;
                } else if (dist[mate] == inf) {
                    dist[mate] = dist[u] + 1;
                    ready.add(mate);
                }
            }
        }
        return distNil != inf;
    }

    private boolean dfs(int u) {
        for (int v : adj.get(u)) {
            int mate = pairV[v];
            if (mate == FREE) {
                if (dist[u] + 1 != distNil) continue;
            } else if (dist[mate] != dist[u] + 1 || !dfs(mate)) {
                continue;
            }

            pairU[u] = v;
            pairV[v] = u;
            return true;
        }

        dist[u] = inf;
        return false;
    }

    int maximumMatching() {
        int matching = 0;
        while (bfs()) {
            for (int u = 0; u < leftSize; ++u)
                if (pairU[u] == FREE && dfs(u)) ++matching;
        }
        return matching;
    }

    int[] leftPartners() { return pairU.clone(); }

    public static void main(String[] args) {
        List<List<Integer>> adj = Arrays.asList(
            Arrays.asList(0, 2), Arrays.asList(1, 3),
            Collections.singletonList(0), Collections.singletonList(1)
        );
        HopcroftKarp matcher = new HopcroftKarp(adj, 4);
        System.out.println("Matching size: " + matcher.maximumMatching());
        System.out.println("Left partners: " + Arrays.toString(matcher.leftPartners()));
    }
}

Complexity and Alternatives

Within one phase, BFS scans the bipartite edges to build layers. DFS scans admissible edges while failed left vertices are marked unreachable for the rest of that phase. Thus a phase costs $O(E)$, not one full search per augmented path.

$$O(E)\text{ per phase}\times O(\sqrt{V})\text{ phases}=O(E\sqrt{V})\text{ time}.$$ $$\text{Space}=O(V+E).$$

Here $V=|L|+|R|$. The space includes adjacency lists, two partner arrays, the BFS distance array, and the queue. For a dense bipartite graph with $|L|=|R|=n$, $V=2n$ and $E$ may be $n^2$; the edge count still dominates the actual scanning work.

ProblemTypical algorithmTimeWhat changes?
Unweighted bipartite maximum matchingHopcroft–Karp$O(E\sqrt{V})$Best fit for this page's model
One augmenting path per searchKuhn-style DFS$O(VE)$ worst caseSimpler; often fine for small inputs
Bipartite matching as flowMax-flow reductionDepends on flow algorithmUseful when capacities or extra constraints matter
Weighted bipartite assignmentHungarian / min-cost flowProblem-dependentOptimizes total weight, not only cardinality
General non-bipartite matchingBlossom-family algorithmsProblem-dependentMust handle odd cycles

Real-World Applications

Hopcroft–Karp answers a precise question: what is the largest number of compatible left–right pairs when every acceptable pair has equal value?

Eligibility assignment

Match applicants to roles, volunteers to shifts, or students to projects using yes/no compatibility edges.

Resource allocation

Pair jobs with machines that can execute them when every machine and job may be used at most once.

Graph reductions

Use bipartite matching inside minimum vertex cover, path-cover, and scheduling reductions.

Structural Payoff

Minimum Vertex Cover in Bipartite Graphs

Kőnig's theorem says the maximum matching size equals the minimum vertex-cover size in a bipartite graph. After Hopcroft–Karp finds a maximum matching, one additional alternating reachability search can construct a minimum vertex cover—turning a pairing routine into a certificate for a seemingly different optimization problem.

AssignmentsDAG Path CoverVertex CoverScheduling

Pitfalls and Implementation Checklist

Common Failure Modes

  • Mixing the partitions: adjacency lists should run from left IDs to right IDs, with separate partner arrays.
  • Starting BFS from matched left vertices: only free left vertices can begin augmenting paths.
  • Searching below the first free-right layer: deeper paths are not part of the current shortest-path phase and can break the complexity argument.
  • Ignoring DFS layer checks: DFS must move from distance $d$ to $d+1$ through the matched partner, and it may finish only at distNil.
  • Omitting dead-end pruning: setting dist[u] = INF after DFS failure prevents repeated rescans during the phase.
  • Counting a flip incorrectly: increment matching size once per successful augmenting path, not once per edge changed.
  • Using it for weighted preference: maximum cardinality ignores which maximum matching has the best total score.
  • Overlooking recursion depth: a very long alternating path may require an iterative DFS or a larger safe stack.

Validate the Output

For every matched $u$, confirm that $(u,\text{pairU}[u])$ is a real edge; verify pair arrays agree in both directions; ensure no partner appears twice; and check that the reported size equals the number of non-free entries. On small random graphs, compare against brute force or a trusted max-flow implementation.

Exercises

  1. Trace the worked graph from an empty matching. Show that phase one finds $A-1$ and $B-2$, while phase two finds both length-three augmenting paths.
  2. Remove edge $B-4$. Redraw the layered graph and determine the matching returned after the next phase.
  3. Explain why two augmenting paths that share a vertex cannot safely be flipped as independent updates.
  4. Instrument the implementation to print distNil, the number of successful DFS calls, and matching size after every phase.
  5. Construct the minimum vertex cover from the final matching using alternating reachability.
  6. Compare Hopcroft–Karp with a one-path-at-a-time DFS matcher on increasingly large sparse graphs.
  7. Challenge: replace recursive DFS with explicit stack frames while preserving path reconstruction and layer pruning.

Historical Note

John Hopcroft and Richard Karp introduced the algorithm in 1973. Its enduring idea is broader than matching: when many shortest improvements are independent, construct a layered search space once and apply a maximal batch before rebuilding it. That phase-based pattern reappears throughout advanced flow and matching algorithms.