Edmonds-Karp
Edmonds-Karp is the Ford-Fulkerson method specialized so that each augmenting path is the shortest (in terms of edge count) path from source to sink in the residual graph, found by Breadth-First Search. This single design choice fixes two flaws of generic Ford-Fulkerson at once: (1) it eliminates the dependence of the running time on the maximum-flow value, giving a guaranteed
O(V · E²)regardless of capacities, and (2) it terminates correctly on graphs with irrational capacities, where naive depth-first augmenting can fail to converge. The algorithm was published by Edmonds & Karp in 1972, but the sameO(V · E²)bound for the BFS-shortest-path variant had been independently discovered by Dinitz (often anglicized “Dinic”) in 1970 — a fact that has caused decades of name confusion. Edmonds-Karp is the algorithm interviewers expect when they say “implement max flow”; it is also the natural first stop on the road to the asymptotically faster Dinic’s Algorithm.
1. Intuition — Why “Shortest” Helps
The root inefficiency of generic Ford-Fulkerson is that bad path choices can waste effort. A pathological example: capacities of 10^9 on most edges and a single bottleneck edge of capacity 1 between two intermediate vertices. A depth-first search may keep choosing paths that traverse the bottleneck, each pushing only 1 unit, requiring 2·10^9 iterations even though the optimal flow is achievable in two pushes. The dependency on |f*| (max-flow value) makes the algorithm pseudopolynomial — fast in problem-graph terms but exponential in input bit-length when capacities are large.
Edmonds-Karp’s insight is structural rather than capacity-driven. By always augmenting along a shortest s → t path in the current residual graph, two invariants hold across iterations:
- The distance from
sto any vertexvin the residual graph is non-decreasing as the algorithm progresses. Once a vertex is “k hops away from s,” it stays at least k hops away forever. - Each edge can become “saturated” (its forward residual hits zero, removing it from the residual graph) at most
O(V)times — because each saturation requires the distance to its tail vertex to strictly increase, and that distance can only grow up toV.
Multiplying these together: each of the O(E) edges is saturated at most O(V) times, and each saturation corresponds to at most one augmenting iteration with that edge as bottleneck, so the total number of augmenting iterations is O(V · E). Each iteration costs O(E) for the BFS, giving O(V · E²). Crucially, none of this depends on capacity values — the analysis is purely combinatorial. That’s the magic.
The “explain to a child” version: imagine the network as a multi-day flood evacuation. Every day, you rescue people along the shortest available route. Each road can only get blocked finitely many times, because every time it gets unblocked-and-reused you’ve had to restructure the road network, and you can’t keep restructuring forever — there are only so many possible “shapes” of road. So the total number of evacuation days is bounded, regardless of how many people each truck carries.
2. The Algorithm
edmonds_karp(G, s, t):
initialize f(u, v) := 0 for all (u, v)
while True:
path := bfs_shortest_path_in_residual_graph(s, t)
if path is empty:
return total flow
b := bottleneck capacity of path
push b along path (update forward and reverse residuals)
total flow += b
The only difference from the generic Ford-Fulkerson template (see Maximum Flow §3) is the augmenting-path choice: BFS is mandatory, not optional. BFS visits vertices in increasing order of edge-count distance from s, so the first time we pop t we have the shortest such path.
3. Tiny Worked Example
Consider a 4-vertex network demonstrating where Edmonds-Karp visibly beats DFS-based augmenting. Edges:
s → a : 1000
s → b : 1000
a → b : 1
a → t : 1000
b → t : 1000
Maximum flow is 2000 (push 1000 along s→a→t, 1000 along s→b→t).
DFS-based Ford-Fulkerson, worst case:
If DFS happens to follow s → a → b → t first (using edge a→b), bottleneck is 1, push 1. Now reverse residual b → a is 1. Next iteration, DFS finds s → b → a → t (using the reverse of a→b), bottleneck 1, push 1. Continuing, DFS oscillates between forward and reverse uses of edge a→b, each iteration pushing 1, taking 2000 iterations to converge.
Edmonds-Karp (BFS):
BFS from s finds the shortest path. From s, we can reach a and b in one hop, and t in two hops via s→a→t or s→b→t (length 2). Path s→a→b→t is length 3, longer, so BFS will not pick it. Iteration 1: pick s→a→t, bottleneck 1000, push 1000. Iteration 2: pick s→b→t, bottleneck 1000, push 1000. Iteration 3: BFS finds no s→t path of positive residual edges (the only remaining residual s→t route goes through the awkward a→b edge of capacity 1, and even that has been disconnected from t because a→t and b→t are saturated). Done in 2 iterations.
The ratio 2000 / 2 = 1000 reflects exactly the capacity disparity that DFS-based augmenting amplifies and BFS sidesteps.
4. Pseudocode
edmonds_karp(G, s, t):
initialize residual graph G_f from G
total := 0
while True:
# BFS in G_f, recording parent pointers
parent := { s: None }
queue := [s]
while queue not empty and t not in parent:
u := dequeue
for v with c_f(u, v) > 0 and v not in parent:
parent[v] := u
enqueue v
if t not in parent:
return total # no augmenting path → done
# reconstruct path and find bottleneck
b := ∞
v := t
while parent[v] is not None:
u := parent[v]
b := min(b, c_f(u, v))
v := u
# augment along path
v := t
while parent[v] is not None:
u := parent[v]
c_f(u, v) -= b
c_f(v, u) += b
v := u
total := total + b
5. Python Implementation
from collections import defaultdict, deque
def edmonds_karp(edges, s, t):
"""edges: list of (u, v, capacity). Returns max flow value from s to t."""
# build residual: dict-of-dicts so residual[u][v] is the capacity left
cap = defaultdict(lambda: defaultdict(int))
nodes = {s, t}
for u, v, c in edges:
cap[u][v] += c
cap[v][u] += 0 # ensure key exists (no-op default)
nodes.update([u, v])
total = 0
while True:
# BFS for shortest residual path
parent = {s: None}
q = deque([s])
while q and t not in parent:
u = q.popleft()
for v, c in cap[u].items():
if c > 0 and v not in parent:
parent[v] = u
if v == t:
break
q.append(v)
if t not in parent:
return total
# find bottleneck along reconstructed path
b, v = float('inf'), t
while parent[v] is not None:
u = parent[v]
b = min(b, cap[u][v])
v = u
# augment
v = t
while parent[v] is not None:
u = parent[v]
cap[u][v] -= b
cap[v][u] += b
v = u
total += bImplementation notes.
- We dictionary-encode the residual graph because in interview settings vertex labels are often strings, and the cleanest data structure is
cap[u][v]. For numerical-vertex hot-loop production code, replace withcap = [[0] * n for _ in range(n)](an adjacency matrix) plus an adjacency list. - The BFS records
parent[v] = uonly when first discoveringv; this guaranteesparentchains form a shortest-path tree. - The early
breakwhen reachingtis an optimization — once we’ve foundt, we don’t need to keep BFSing. - The augment loop walks
parentpointers fromtback tostwice (once to find bottleneck, once to apply it). You can fuse them into one pass by storing per-edge references along the path, but the two-pass version is more readable and asymptotically identical.
6. Complexity Analysis
Theorem (Edmonds & Karp, 1972). Edmonds-Karp computes the maximum flow in O(V · E²) time, regardless of edge capacities (including irrational ones).
The proof is one of the classic combinatorial arguments in algorithms; it’s worth understanding rather than just memorizing.
6.1 Lemma 1 — Distances are monotone
Let d_f(s, v) denote the BFS-distance (in number of edges) from s to v in the residual graph after some flow f. Augmenting along a shortest residual path produces a new flow f'; we claim d_{f'}(s, v) ≥ d_f(s, v) for every v.
Proof sketch. Suppose for contradiction that some vertex v becomes closer to s in G_{f'} than in G_f. Pick v to be the closest such vertex (i.e., minimum d_{f'}(s, v)). Consider the shortest path from s to v in G_{f'}; let u be the predecessor of v on this path. By choice of v as the closest violator, d_{f'}(s, u) ≥ d_f(s, u). Now examine the edge (u, v):
- Case A:
(u, v)exists inG_f. Thend_f(s, v) ≤ d_f(s, u) + 1 ≤ d_{f'}(s, u) + 1 = d_{f'}(s, v), contradicting our assumption. - Case B:
(u, v)exists inG_{f'}but not inG_f. This means the augmenting path used edge(v, u)(in that direction), creating reverse residual(u, v). Since augmentation was along a shortest path inG_f, edge(v, u)lies on a shortest path, sod_f(s, u) = d_f(s, v) + 1, i.e.,d_f(s, v) = d_f(s, u) − 1 ≤ d_{f'}(s, u) − 1 = d_{f'}(s, v) − 2 < d_{f'}(s, v). Again contradicting the assumption.
Both cases contradict, so no such v exists. □
6.2 Lemma 2 — Each edge is saturated O(V) times
When an edge (u, v) becomes “critical” (its residual capacity drops to zero on some augmenting path), the path used has the form s → ⋯ → u → v → ⋯ → t of length d_f(s, u) + 1 + d_f(v, t) = d_f(s, t) since it’s a shortest path. After augmentation, (u, v) disappears from the residual graph until some future augmenting path uses the reverse edge (v, u). When that happens, that future path has the form s → ⋯ → v → u → ⋯ → t, so d_{f'}(s, u) = d_{f'}(s, v) + 1. Combined with Lemma 1 (d_{f'}(s, v) ≥ d_f(s, v) = d_f(s, u) + 1), we get d_{f'}(s, u) ≥ d_f(s, u) + 2.
So between successive saturations of the same edge (u, v), the distance d(s, u) strictly increases by at least 2. Since distances are bounded above by V − 1 (the longest possible simple path), each edge can be saturated at most O(V) times.
6.3 Putting it together
There are O(E) distinct edges in the residual graph. Each is saturated at most O(V) times. Each augmenting iteration saturates at least one edge (the bottleneck). Therefore, the number of augmenting iterations is O(V · E). Each iteration runs a BFS in O(V + E) = O(E) and does a path walk in O(V) ≤ O(E). Total: O(V · E · E) = O(V · E²). □
6.4 Practical caveats
O(V · E²)is the worst case; on most inputs Edmonds-Karp finishes far faster.- For dense graphs (
E = Θ(V²)), the bound becomesO(V⁵), which is poor. Switch to Dinic’s Algorithm forO(V²·E)or push-relabel forO(V²·√E). - For unit-capacity graphs (every capacity is 0 or 1, common in Bipartite Matching), Edmonds-Karp is
O(E·√V)if you specifically use Dinic’s blocking-flow refinement; pure Edmonds-Karp isO(V·E)on unit-capacity graphs, still fine but not optimal.
7. Variants and Connections
7.1 Choosing BFS vs other strategies
- BFS (Edmonds-Karp): simplest with provable polynomial guarantees. Default for interviews.
- Fattest path (Edmonds-Karp 1972 also analyzed this): at each step, find the path with maximum bottleneck (using a Dijkstra-like priority queue with
max-minsemantics). Iterations:O(E · log U)whereUis the max capacity. Often faster in practice but algorithmically more complex. - Capacity scaling: binary-search the threshold; only consider edges with residual capacity ≥ threshold; halve threshold. Iterations:
O(E · log U). - Level graph + blocking flow: Dinic’s Algorithm. Asymptotically better.
7.2 Why Edmonds-Karp is the “standard” interview answer
Compared to push-relabel and Dinic’s, Edmonds-Karp:
- Has the simplest implementation (BFS + adjacency-map updates)
- Has an elegant, teachable correctness/complexity proof
- Uses only ideas already in the interviewer’s working memory (Breadth-First Search + residual graph + augmenting path)
- Is rarely the bottleneck — most interview problems with max-flow flavor have small enough graphs that
O(V·E²)is plenty
If the interviewer presses for asymptotic improvements, mention Dinic’s or push-relabel; otherwise stick with Edmonds-Karp.
8. Common Interview Problems
| Problem | How Edmonds-Karp applies |
|---|---|
| LC 1947 — Maximum Compatibility Score Sum | Reduce to bipartite-matching-with-weights → max-flow / Hungarian |
| LC 1820 — Maximum Number of Accepted Invitations | Bipartite matching → unit-capacity max-flow |
| LC 1349 — Maximum Students Taking Exam | Bipartite matching on grid (often DP, but max-flow is valid) |
| Classic: maximum bipartite matching | See Bipartite Matching |
| Classic: edge-disjoint paths | Set every capacity to 1; max flow counts disjoint paths |
| Classic: minimum cut between two cities | Run max-flow, then identify residual-graph reachable set from s |
| Classic: project selection | Reduce profit-maximization to min-cut |
| Classic: image segmentation (graph cut) | s = “foreground source”, t = “background sink”; min-cut partitions |
9. Pitfalls
9.1 Using DFS by accident
Trivially silently breaks the complexity guarantee. Test: print the number of augmenting iterations on a contrived high-capacity graph; if it’s proportional to |f*|, you’re using DFS.
9.2 BFS that doesn’t respect residual capacity
The neighbor expansion must check c_f(u, v) > 0, not whether (u, v) is an edge in the original graph. Many bugs trace to BFSing over the original adjacency list and then failing to find paths that pass through reverse edges.
9.3 Updating residuals only in one direction
After augmenting, both c_f(u, v) -= b and c_f(v, u) += b must occur. Forgetting either is a silent correctness bug.
9.4 Repeatedly recreating data structures
Each iteration creates a new parent map and a new BFS queue — this is correct. But pre-allocating a single mutable parent array and clearing it between iterations is significantly faster on dense graphs. For interviews, idiomatic per-iteration construction is fine.
9.5 Believing the algorithm “doesn’t work” on irrational capacities
Edmonds-Karp’s O(V·E²) bound is combinatorial and does not reference capacity values. It works correctly on any rational, irrational, or integer capacity. The only constraint is that arithmetic on capacities must be exact — which means floating-point capacities will give floating-point flows, and rounding errors can cause weird behavior. Use rationals or scaled integers in production.
9.6 Not handling disconnected graphs
If t is unreachable from s in the original graph, Edmonds-Karp’s first BFS finds no path and immediately returns 0. Correct, but worth confirming on test inputs.
9.7 Confusing “shortest path” with “fewest hops” vs “min-weight path”
In Edmonds-Karp, “shortest” specifically means fewest residual edges. It is not the path of minimum capacity sum, nor the fattest (max-min) path. Substituting Dijkstra-on-capacity for BFS does not give Edmonds-Karp’s bound — it gives a different (also-valid) algorithm with different complexity.
10. Diagram — Edmonds-Karp at Work
flowchart LR s((s)) -->|10| a((a)) s -->|10| b((b)) a -->|5| t((t)) a -->|1| b b -->|10| t classDef visited fill:#bef,stroke:#06c,color:#000; class s,a,b,t visited;
What this diagram shows. The original capacity graph used in §4 of Maximum Flow. Edmonds-Karp’s BFS from s reaches a and b at distance 1, and reaches t at distance 2 via s→a→t or s→b→t. The path s→a→b→t is at distance 3 and is never chosen until iterations after the shorter paths are saturated. This selection discipline is precisely what bounds the iteration count to O(V · E) regardless of how large the numerical capacities are.
11. Open Questions
- Why don’t we use the fattest-path strategy in practice if it’s also
O(E · log U)per iteration with a similar total bound? (In practice, BFS is much simpler to code and the constant factor on the priority queue dominates for smallU.) - Is there a simple proof that Edmonds-Karp’s bound is tight? (Yes: a sequence of “zigzag” graphs achieves
Θ(V · E²)on adversarial inputs; see Galil 1980 for an explicit construction.) - How does Edmonds-Karp parallelize? (Poorly. The augmenting-path framework is inherently sequential. Push-relabel is the parallel-friendly choice.)
12. See Also
- Maximum Flow — the meta-algorithm Edmonds-Karp specializes
- Dinic’s Algorithm —
O(V²·E), the next step up in sophistication - Min-Cut Max-Flow Theorem — what the final flow witnesses
- Bipartite Matching — biggest application: cap=1 on every edge → matching
- Hopcroft-Karp — bipartite matching specialization,
O(E·√V) - Breadth-First Search — the inner loop of Edmonds-Karp
- Depth-First Search — what not to use here (Dinic’s combines it with BFS layering instead)
- Big-O Notation
- SWE Interview Preparation MOC