Edmonds-Karp
Edmonds-Karp is Ford-Fulkerson with BFS: always augment along a shortest path, measured in number of arcs. That single rule bounds the number of augmentations by O(V·E), independent of the capacities.
Learning goals
- Implement Edmonds-Karp with BFS and parent arcs
- Explain the O(V·E²) bound
- Update the residual arcs along the path correctly
Prerequisites
If you must route water through a pipe network, using the route with the fewest pipes first keeps you from wandering through long zig-zag detours that waste effort.
Shortest augmenting BFS
Edmonds-Karp changes only one thing: the search for an augmenting path is a **BFS**, so the path it finds has the fewest arcs among all residual paths. Everything else is Ford-Fulkerson.
Load the “Edmonds-Karp on the same network” preset: 4 augmentations reach the maximum flow 19 on the 6-vertex sample network (Ford-Fulkerson with DFS needs 5 on it). And on “Edmonds-Karp avoids the middle edge” two augmentations of 9 units suffice where DFS needed four rounds.
BFS records, for every vertex, the arc it was reached by (viaArc); the path is rebuilt by walking back from the sink.
1static int edmondsKarp(Net g, int s, int t) {2int flow = 0;3while (true) {4int[] viaArc = new int[g.n];5Arrays.fill(viaArc, -1);6Deque<Integer> queue = new ArrayDeque<>(List.of(s));7while (!queue.isEmpty() && viaArc[t] == -1) { // BFS: the path with fewest arcs8int u = queue.poll();9for (int id : g.adj[u]) {10int v = g.to[id];11if (v != s && viaArc[v] == -1 && g.cap[id] > 0) { viaArc[v] = id; queue.add(v); }12}13}14if (viaArc[t] == -1) return flow;15int bottleneck = Integer.MAX_VALUE;16for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) bottleneck = Math.min(bottleneck, g.cap[viaArc[v]]);17for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) { g.cap[viaArc[v]] -= bottleneck; g.cap[viaArc[v] ^ 1] += bottleneck; }18flow += bottleneck;19}20}
Worst
O(V · E²)
Space
O(V + E)
Maximum flow: Ford-Fulkerson, Edmonds-Karp, Dinic, push-relabel
Interactive · edit the inputedge label: flow / capacity · 6 vertices · 9 edges
source
sink
residual arcs u→v : remaining capacity
Every edge starts with flow 0, so every arc has its full capacity as residual capacity and every reverse arc has 0. Edmonds-Karp (BFS paths) looks for ways to send more from 0 to 5. The label on an edge is flow / capacity.
1static class Net {2final int n;3final List<Integer>[] adj; // adj[u]: ids of the arcs leaving u4final int[] to, cap; // arc k and arc k ^ 1 are each other's reverse5int arcs = 0;6@SuppressWarnings("unchecked")7Net(int n, int maxEdges) {8this.n = n; adj = new List[n]; to = new int[2 * maxEdges]; cap = new int[2 * maxEdges];9for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();10}11void addEdge(int u, int v, int c) {12to[arcs] = v; cap[arcs] = c; adj[u].add(arcs++); // forward arc with the capacity13to[arcs] = u; cap[arcs] = 0; adj[v].add(arcs++); // reverse arc: capacity 0 until flow is pushed14}15}16static int fordFulkerson(Net g, int s, int t) {17int flow = 0;18while (true) {19int pushed = dfs(g, s, t, Integer.MAX_VALUE, new boolean[g.n]); // any augmenting path, found depth-first20if (pushed == 0) return flow;21flow += pushed;22}23}24static int dfs(Net g, int u, int t, int f, boolean[] seen) {25if (u == t) return f;26seen[u] = true;27for (int id : g.adj[u]) {28int v = g.to[id];29if (seen[v] || g.cap[id] == 0) continue;30int d = dfs(g, v, t, Math.min(f, g.cap[id]), seen);31if (d > 0) { g.cap[id] -= d; g.cap[id ^ 1] += d; return d; } // use d units, open d units of reverse capacity32}33return 0;34}35static int edmondsKarp(Net g, int s, int t) {36int flow = 0;37while (true) {38int[] viaArc = new int[g.n];39Arrays.fill(viaArc, -1);40Deque<Integer> queue = new ArrayDeque<>(List.of(s));41while (!queue.isEmpty() && viaArc[t] == -1) { // BFS: the path with fewest arcs42int u = queue.poll();43for (int id : g.adj[u]) {44int v = g.to[id];45if (v != s && viaArc[v] == -1 && g.cap[id] > 0) { viaArc[v] = id; queue.add(v); }46}47}48if (viaArc[t] == -1) return flow;49int bottleneck = Integer.MAX_VALUE;50for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) bottleneck = Math.min(bottleneck, g.cap[viaArc[v]]);51for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) { g.cap[viaArc[v]] -= bottleneck; g.cap[viaArc[v] ^ 1] += bottleneck; }52flow += bottleneck;53}54}55static int dinic(Net g, int s, int t) {56int flow = 0;57int[] level = new int[g.n];58while (levels(g, s, t, level)) { // new level graph; stop when t is unreachable59int[] next = new int[g.n]; // next arc to try at each vertex60for (int f; (f = blocking(g, s, t, Integer.MAX_VALUE, level, next)) > 0; ) flow += f;61}62return flow;63}64static boolean levels(Net g, int s, int t, int[] level) {65Arrays.fill(level, -1);66level[s] = 0;67Deque<Integer> queue = new ArrayDeque<>(List.of(s));68while (!queue.isEmpty()) {69int u = queue.poll();70for (int id : g.adj[u]) if (g.cap[id] > 0 && level[g.to[id]] == -1) { level[g.to[id]] = level[u] + 1; queue.add(g.to[id]); }71}72return level[t] != -1;73}74static int blocking(Net g, int u, int t, int f, int[] level, int[] next) {75if (u == t) return f;76for (; next[u] < g.adj[u].size(); next[u]++) {77int id = g.adj[u].get(next[u]), v = g.to[id];78if (g.cap[id] == 0 || level[v] != level[u] + 1) continue; // only arcs one level deeper79int d = blocking(g, v, t, Math.min(f, g.cap[id]), level, next);80if (d > 0) { g.cap[id] -= d; g.cap[id ^ 1] += d; return d; }81}82return 0;83}84static int pushRelabel(Net g, int s, int t) {85int[] height = new int[g.n], excess = new int[g.n];86height[s] = g.n; // the source starts as high as possible87Deque<Integer> active = new ArrayDeque<>();88for (int id : g.adj[s]) { // saturate every arc out of the source89int c = g.cap[id], v = g.to[id];90if (c == 0) continue;91g.cap[id] = 0; g.cap[id ^ 1] += c; excess[v] += c;92if (v != t && excess[v] == c) active.add(v);93}94while (!active.isEmpty()) discharge(g, active.poll(), s, t, height, excess, active);95return excess[t];96}97static void discharge(Net g, int u, int s, int t, int[] height, int[] excess, Deque<Integer> active) {98while (excess[u] > 0) {99boolean pushed = false;100for (int id : g.adj[u]) {101int v = g.to[id];102if (g.cap[id] > 0 && height[u] == height[v] + 1) { // push only downhill, by exactly one level103int d = Math.min(excess[u], g.cap[id]);104g.cap[id] -= d; g.cap[id ^ 1] += d; excess[u] -= d; excess[v] += d;105if (v != s && v != t && excess[v] == d) active.add(v);106pushed = true;107if (excess[u] == 0) break;108}109}110if (!pushed) { // stuck: relabel one above its lowest residual neighbour111int lowest = Integer.MAX_VALUE;112for (int id : g.adj[u]) if (g.cap[id] > 0) lowest = Math.min(lowest, height[g.to[id]]);113height[u] = lowest + 1;114}115}116}
Live state
n
6
edges
9
source
0
sink
5
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L16 | n=6 edges=9 source=0 sink=5 | Every edge starts with flow 0, so every arc has its full capacity as residual capacity and every reverse arc has 0. Edmonds-Karp (BFS paths) looks for ways to send more from 0 to 5. The label on an edge is flow / capacity. |
| 2 | L48 | path=0→1→3→5 bottleneck=4 flow=0 | Breadth-first search (fewest arcs) finds the path 0→1→3→5 with 3 arcs. Its smallest residual capacity is 4. |
| 3 | L51 | bottleneck=4 flow=4 | Push 4 along the path. Flow value is now 4. |
| 4 | L48 | path=0→1→4→5 bottleneck=6 flow=4 | Breadth-first search (fewest arcs) finds the path 0→1→4→5 with 3 arcs. Its smallest residual capacity is 6. |
| 5 | L51 | bottleneck=6 flow=10 | Push 6 along the path. Flow value is now 10. |
| 6 | L48 | path=0→2→4→5 bottleneck=4 flow=10 | Breadth-first search (fewest arcs) finds the path 0→2→4→5 with 3 arcs. Its smallest residual capacity is 4. |
| 7 | L51 | bottleneck=4 flow=14 | Push 4 along the path. Flow value is now 14. |
| 8 | L48 | path=0→2→4→3→5 bottleneck=5 flow=14 | Breadth-first search (fewest arcs) finds the path 0→2→4→3→5 with 4 arcs. Its smallest residual capacity is 5. |
| 9 | L51 | bottleneck=5 flow=19 | Push 5 along the path. Flow value is now 19. |
| 10 | L48 | flow=19 | No path with positive residual capacity from 0 to 5 remains: the search ends. |
| 11 | L16 | return=19 cut_capacity=19 augmentations=4 | Maximum flow = 19. The vertices still reachable from 0 in the residual graph, {0, 2}, form the minimum cut: the edges leaving that set (0→1, 2→4) are all saturated and their capacities add up to 19, the same number. |
O(VE²) bound
Why O(V · E²)? Three facts. (1) Distances from the source in the residual graph never decrease during the algorithm: augmenting along a shortest path only adds backward arcs that point from a vertex to one at a smaller distance. (2) In each augmentation at least one arc on the path, a **critical arc**, is saturated, and it cannot be critical again until its endpoint distances have grown. (3) An arc can be critical at most V/2 times, because distances are below V.
There are at most E arcs, each critical at most V/2 times, and each augmentation makes some arc critical, so there are at most about V·E/2 augmentations. Each costs one BFS, O(E), giving O(V · E²) in total. The test suite for this course checks the augmentation count against V·E/2 on thousands of random networks.
The bound does not mention capacities at all, so large or even real-valued capacities do no harm.
| Quantity | Bound |
|---|---|
| Distance from s to any vertex | never decreases, at most V − 1 |
| Times one arc is critical | at most V / 2 |
| Augmentations | at most V · E / 2 |
| Work per augmentation (one BFS) | O(E) |
| Total | O(V · E²) |
Residual updates
The update after finding a path is the same two lines for every algorithm: subtract the bottleneck from every forward arc on the path and add it to the paired reverse arc. Because the pair is k and k ^ 1, the code treats forward and backward moves identically.
Get the order right: compute the bottleneck over the whole path first, and only then modify arcs. Changing capacities while scanning would corrupt the minimum.
Watch the residual row under the graph in the animation. After each augmentation the arcs on the path shrink and their reverse arcs (marked * in the animation's path row) appear or grow; arcs that reach 0 disappear from the list.
1static int augmentAlongPath(Net g, int[] viaArc, int s, int t) { // viaArc[v]: the arc used to reach v on the s-t path2int bottleneck = Integer.MAX_VALUE;3for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) bottleneck = Math.min(bottleneck, g.cap[viaArc[v]]);4for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) { g.cap[viaArc[v]] -= bottleneck; g.cap[viaArc[v] ^ 1] += bottleneck; }5return bottleneck;6}
Maximum flow: Ford-Fulkerson, Edmonds-Karp, Dinic, push-relabel
Interactive · edit the inputedge label: flow / capacity · 6 vertices · 9 edges
source
sink
residual arcs u→v : remaining capacity
Every edge starts with flow 0, so every arc has its full capacity as residual capacity and every reverse arc has 0. Edmonds-Karp (BFS paths) looks for ways to send more from 0 to 5. The label on an edge is flow / capacity.
1static class Net {2final int n;3final List<Integer>[] adj; // adj[u]: ids of the arcs leaving u4final int[] to, cap; // arc k and arc k ^ 1 are each other's reverse5int arcs = 0;6@SuppressWarnings("unchecked")7Net(int n, int maxEdges) {8this.n = n; adj = new List[n]; to = new int[2 * maxEdges]; cap = new int[2 * maxEdges];9for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();10}11void addEdge(int u, int v, int c) {12to[arcs] = v; cap[arcs] = c; adj[u].add(arcs++); // forward arc with the capacity13to[arcs] = u; cap[arcs] = 0; adj[v].add(arcs++); // reverse arc: capacity 0 until flow is pushed14}15}16static int fordFulkerson(Net g, int s, int t) {17int flow = 0;18while (true) {19int pushed = dfs(g, s, t, Integer.MAX_VALUE, new boolean[g.n]); // any augmenting path, found depth-first20if (pushed == 0) return flow;21flow += pushed;22}23}24static int dfs(Net g, int u, int t, int f, boolean[] seen) {25if (u == t) return f;26seen[u] = true;27for (int id : g.adj[u]) {28int v = g.to[id];29if (seen[v] || g.cap[id] == 0) continue;30int d = dfs(g, v, t, Math.min(f, g.cap[id]), seen);31if (d > 0) { g.cap[id] -= d; g.cap[id ^ 1] += d; return d; } // use d units, open d units of reverse capacity32}33return 0;34}35static int edmondsKarp(Net g, int s, int t) {36int flow = 0;37while (true) {38int[] viaArc = new int[g.n];39Arrays.fill(viaArc, -1);40Deque<Integer> queue = new ArrayDeque<>(List.of(s));41while (!queue.isEmpty() && viaArc[t] == -1) { // BFS: the path with fewest arcs42int u = queue.poll();43for (int id : g.adj[u]) {44int v = g.to[id];45if (v != s && viaArc[v] == -1 && g.cap[id] > 0) { viaArc[v] = id; queue.add(v); }46}47}48if (viaArc[t] == -1) return flow;49int bottleneck = Integer.MAX_VALUE;50for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) bottleneck = Math.min(bottleneck, g.cap[viaArc[v]]);51for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) { g.cap[viaArc[v]] -= bottleneck; g.cap[viaArc[v] ^ 1] += bottleneck; }52flow += bottleneck;53}54}55static int dinic(Net g, int s, int t) {56int flow = 0;57int[] level = new int[g.n];58while (levels(g, s, t, level)) { // new level graph; stop when t is unreachable59int[] next = new int[g.n]; // next arc to try at each vertex60for (int f; (f = blocking(g, s, t, Integer.MAX_VALUE, level, next)) > 0; ) flow += f;61}62return flow;63}64static boolean levels(Net g, int s, int t, int[] level) {65Arrays.fill(level, -1);66level[s] = 0;67Deque<Integer> queue = new ArrayDeque<>(List.of(s));68while (!queue.isEmpty()) {69int u = queue.poll();70for (int id : g.adj[u]) if (g.cap[id] > 0 && level[g.to[id]] == -1) { level[g.to[id]] = level[u] + 1; queue.add(g.to[id]); }71}72return level[t] != -1;73}74static int blocking(Net g, int u, int t, int f, int[] level, int[] next) {75if (u == t) return f;76for (; next[u] < g.adj[u].size(); next[u]++) {77int id = g.adj[u].get(next[u]), v = g.to[id];78if (g.cap[id] == 0 || level[v] != level[u] + 1) continue; // only arcs one level deeper79int d = blocking(g, v, t, Math.min(f, g.cap[id]), level, next);80if (d > 0) { g.cap[id] -= d; g.cap[id ^ 1] += d; return d; }81}82return 0;83}84static int pushRelabel(Net g, int s, int t) {85int[] height = new int[g.n], excess = new int[g.n];86height[s] = g.n; // the source starts as high as possible87Deque<Integer> active = new ArrayDeque<>();88for (int id : g.adj[s]) { // saturate every arc out of the source89int c = g.cap[id], v = g.to[id];90if (c == 0) continue;91g.cap[id] = 0; g.cap[id ^ 1] += c; excess[v] += c;92if (v != t && excess[v] == c) active.add(v);93}94while (!active.isEmpty()) discharge(g, active.poll(), s, t, height, excess, active);95return excess[t];96}97static void discharge(Net g, int u, int s, int t, int[] height, int[] excess, Deque<Integer> active) {98while (excess[u] > 0) {99boolean pushed = false;100for (int id : g.adj[u]) {101int v = g.to[id];102if (g.cap[id] > 0 && height[u] == height[v] + 1) { // push only downhill, by exactly one level103int d = Math.min(excess[u], g.cap[id]);104g.cap[id] -= d; g.cap[id ^ 1] += d; excess[u] -= d; excess[v] += d;105if (v != s && v != t && excess[v] == d) active.add(v);106pushed = true;107if (excess[u] == 0) break;108}109}110if (!pushed) { // stuck: relabel one above its lowest residual neighbour111int lowest = Integer.MAX_VALUE;112for (int id : g.adj[u]) if (g.cap[id] > 0) lowest = Math.min(lowest, height[g.to[id]]);113height[u] = lowest + 1;114}115}116}
Live state
n
6
edges
9
source
0
sink
5
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L16 | n=6 edges=9 source=0 sink=5 | Every edge starts with flow 0, so every arc has its full capacity as residual capacity and every reverse arc has 0. Edmonds-Karp (BFS paths) looks for ways to send more from 0 to 5. The label on an edge is flow / capacity. |
| 2 | L48 | path=0→1→3→5 bottleneck=4 flow=0 | Breadth-first search (fewest arcs) finds the path 0→1→3→5 with 3 arcs. Its smallest residual capacity is 4. |
| 3 | L51 | bottleneck=4 flow=4 | Push 4 along the path. Flow value is now 4. |
| 4 | L48 | path=0→1→4→5 bottleneck=6 flow=4 | Breadth-first search (fewest arcs) finds the path 0→1→4→5 with 3 arcs. Its smallest residual capacity is 6. |
| 5 | L51 | bottleneck=6 flow=10 | Push 6 along the path. Flow value is now 10. |
| 6 | L48 | path=0→2→4→5 bottleneck=4 flow=10 | Breadth-first search (fewest arcs) finds the path 0→2→4→5 with 3 arcs. Its smallest residual capacity is 4. |
| 7 | L51 | bottleneck=4 flow=14 | Push 4 along the path. Flow value is now 14. |
| 8 | L48 | path=0→2→4→3→5 bottleneck=5 flow=14 | Breadth-first search (fewest arcs) finds the path 0→2→4→3→5 with 4 arcs. Its smallest residual capacity is 5. |
| 9 | L51 | bottleneck=5 flow=19 | Push 5 along the path. Flow value is now 19. |
| 10 | L48 | flow=19 | No path with positive residual capacity from 0 to 5 remains: the search ends. |
| 11 | L16 | return=19 cut_capacity=19 augmentations=4 | Maximum flow = 19. The vertices still reachable from 0 in the residual graph, {0, 2}, form the minimum cut: the edges leaving that set (0→1, 2→4) are all saturated and their capacities add up to 19, the same number. |
Practice ladder
Now try it yourself
Easy
- Find if Path Exists in Graph
Medium
- Shortest Path in Binary Matrix
- Network Delay Time
Advanced
- Maximum Students Taking Exam
- Minimum Number of Lines to Cover Points
Key takeaways
- Edmonds-Karp = Ford-Fulkerson where the path is found by BFS (fewest arcs).
- At most V·E/2 augmentations, so O(V·E²) time, regardless of capacities.
- Compute the bottleneck first, then update arc k and its reverse k ^ 1 along the path.