Chapter 39: Network Flow and Minimum Cuts
39.04Intermediate / Advanced3 topics13 min2 interactive animations

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

Ford-Fulkerson

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.

39.04.1

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.

Java
1static int edmondsKarp(Net g, int s, int t) {
2 int flow = 0;
3 while (true) {
4 int[] viaArc = new int[g.n];
5 Arrays.fill(viaArc, -1);
6 Deque<Integer> queue = new ArrayDeque<>(List.of(s));
7 while (!queue.isEmpty() && viaArc[t] == -1) { // BFS: the path with fewest arcs
8 int u = queue.poll();
9 for (int id : g.adj[u]) {
10 int v = g.to[id];
11 if (v != s && viaArc[v] == -1 && g.cap[id] > 0) { viaArc[v] = id; queue.add(v); }
12 }
13 }
14 if (viaArc[t] == -1) return flow;
15 int bottleneck = Integer.MAX_VALUE;
16 for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) bottleneck = Math.min(bottleneck, g.cap[viaArc[v]]);
17 for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) { g.cap[viaArc[v]] -= bottleneck; g.cap[viaArc[v] ^ 1] += bottleneck; }
18 flow += bottleneck;
19 }
20}

Worst

O(V · E²)

Space

O(V + E)

Maximum flow: Ford-Fulkerson, Edmonds-Karp, Dinic, push-relabel

Interactive · edit the input

Your input

Try

edge label: flow / capacity · 6 vertices · 9 edges

0

source

1
2
3
4
5

sink

Current path or vertexEdge carrying some flowSaturated edge (flow = capacity)Source side of the minimum cut

residual arcs u→v : remaining capacity

0→1:10
0→2:10
1→2:2
1→3:4
1→4:8
2→4:9
3→5:10
4→3:6
4→5:10
Setup

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.

Step 1 / 11
Java
1static class Net {
2 final int n;
3 final List<Integer>[] adj; // adj[u]: ids of the arcs leaving u
4 final int[] to, cap; // arc k and arc k ^ 1 are each other's reverse
5 int arcs = 0;
6 @SuppressWarnings("unchecked")
7 Net(int n, int maxEdges) {
8 this.n = n; adj = new List[n]; to = new int[2 * maxEdges]; cap = new int[2 * maxEdges];
9 for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
10 }
11 void addEdge(int u, int v, int c) {
12 to[arcs] = v; cap[arcs] = c; adj[u].add(arcs++); // forward arc with the capacity
13 to[arcs] = u; cap[arcs] = 0; adj[v].add(arcs++); // reverse arc: capacity 0 until flow is pushed
14 }
15}
16static int fordFulkerson(Net g, int s, int t) {
17 int flow = 0;
18 while (true) {
19 int pushed = dfs(g, s, t, Integer.MAX_VALUE, new boolean[g.n]); // any augmenting path, found depth-first
20 if (pushed == 0) return flow;
21 flow += pushed;
22 }
23}
24static int dfs(Net g, int u, int t, int f, boolean[] seen) {
25 if (u == t) return f;
26 seen[u] = true;
27 for (int id : g.adj[u]) {
28 int v = g.to[id];
29 if (seen[v] || g.cap[id] == 0) continue;
30 int d = dfs(g, v, t, Math.min(f, g.cap[id]), seen);
31 if (d > 0) { g.cap[id] -= d; g.cap[id ^ 1] += d; return d; } // use d units, open d units of reverse capacity
32 }
33 return 0;
34}
35static int edmondsKarp(Net g, int s, int t) {
36 int flow = 0;
37 while (true) {
38 int[] viaArc = new int[g.n];
39 Arrays.fill(viaArc, -1);
40 Deque<Integer> queue = new ArrayDeque<>(List.of(s));
41 while (!queue.isEmpty() && viaArc[t] == -1) { // BFS: the path with fewest arcs
42 int u = queue.poll();
43 for (int id : g.adj[u]) {
44 int v = g.to[id];
45 if (v != s && viaArc[v] == -1 && g.cap[id] > 0) { viaArc[v] = id; queue.add(v); }
46 }
47 }
48 if (viaArc[t] == -1) return flow;
49 int bottleneck = Integer.MAX_VALUE;
50 for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) bottleneck = Math.min(bottleneck, g.cap[viaArc[v]]);
51 for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) { g.cap[viaArc[v]] -= bottleneck; g.cap[viaArc[v] ^ 1] += bottleneck; }
52 flow += bottleneck;
53 }
54}
55static int dinic(Net g, int s, int t) {
56 int flow = 0;
57 int[] level = new int[g.n];
58 while (levels(g, s, t, level)) { // new level graph; stop when t is unreachable
59 int[] next = new int[g.n]; // next arc to try at each vertex
60 for (int f; (f = blocking(g, s, t, Integer.MAX_VALUE, level, next)) > 0; ) flow += f;
61 }
62 return flow;
63}
64static boolean levels(Net g, int s, int t, int[] level) {
65 Arrays.fill(level, -1);
66 level[s] = 0;
67 Deque<Integer> queue = new ArrayDeque<>(List.of(s));
68 while (!queue.isEmpty()) {
69 int u = queue.poll();
70 for (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 }
72 return level[t] != -1;
73}
74static int blocking(Net g, int u, int t, int f, int[] level, int[] next) {
75 if (u == t) return f;
76 for (; next[u] < g.adj[u].size(); next[u]++) {
77 int id = g.adj[u].get(next[u]), v = g.to[id];
78 if (g.cap[id] == 0 || level[v] != level[u] + 1) continue; // only arcs one level deeper
79 int d = blocking(g, v, t, Math.min(f, g.cap[id]), level, next);
80 if (d > 0) { g.cap[id] -= d; g.cap[id ^ 1] += d; return d; }
81 }
82 return 0;
83}
84static int pushRelabel(Net g, int s, int t) {
85 int[] height = new int[g.n], excess = new int[g.n];
86 height[s] = g.n; // the source starts as high as possible
87 Deque<Integer> active = new ArrayDeque<>();
88 for (int id : g.adj[s]) { // saturate every arc out of the source
89 int c = g.cap[id], v = g.to[id];
90 if (c == 0) continue;
91 g.cap[id] = 0; g.cap[id ^ 1] += c; excess[v] += c;
92 if (v != t && excess[v] == c) active.add(v);
93 }
94 while (!active.isEmpty()) discharge(g, active.poll(), s, t, height, excess, active);
95 return excess[t];
96}
97static void discharge(Net g, int u, int s, int t, int[] height, int[] excess, Deque<Integer> active) {
98 while (excess[u] > 0) {
99 boolean pushed = false;
100 for (int id : g.adj[u]) {
101 int v = g.to[id];
102 if (g.cap[id] > 0 && height[u] == height[v] + 1) { // push only downhill, by exactly one level
103 int d = Math.min(excess[u], g.cap[id]);
104 g.cap[id] -= d; g.cap[id ^ 1] += d; excess[u] -= d; excess[v] += d;
105 if (v != s && v != t && excess[v] == d) active.add(v);
106 pushed = true;
107 if (excess[u] == 0) break;
108 }
109 }
110 if (!pushed) { // stuck: relabel one above its lowest residual neighbour
111 int lowest = Integer.MAX_VALUE;
112 for (int id : g.adj[u]) if (g.cap[id] > 0) lowest = Math.min(lowest, height[g.to[id]]);
113 height[u] = lowest + 1;
114 }
115 }
116}

Live state

0 augmentations0 phases0 pushes0 relabels

n

6

edges

9

source

0

sink

5

No condition evaluated on this step
Execution trace (dry run table)Show 11 steps
StepLineStateWhat happens
1L16n=6 edges=9 source=0 sink=5Every 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.
2L48path=0→1→3→5 bottleneck=4 flow=0Breadth-first search (fewest arcs) finds the path 0→1→3→5 with 3 arcs. Its smallest residual capacity is 4.
3L51bottleneck=4 flow=4Push 4 along the path. Flow value is now 4.
4L48path=0→1→4→5 bottleneck=6 flow=4Breadth-first search (fewest arcs) finds the path 0→1→4→5 with 3 arcs. Its smallest residual capacity is 6.
5L51bottleneck=6 flow=10Push 6 along the path. Flow value is now 10.
6L48path=0→2→4→5 bottleneck=4 flow=10Breadth-first search (fewest arcs) finds the path 0→2→4→5 with 3 arcs. Its smallest residual capacity is 4.
7L51bottleneck=4 flow=14Push 4 along the path. Flow value is now 14.
8L48path=0→2→4→3→5 bottleneck=5 flow=14Breadth-first search (fewest arcs) finds the path 0→2→4→3→5 with 4 arcs. Its smallest residual capacity is 5.
9L51bottleneck=5 flow=19Push 5 along the path. Flow value is now 19.
10L48flow=19No path with positive residual capacity from 0 to 5 remains: the search ends.
11L16return=19 cut_capacity=19 augmentations=4Maximum 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.
39.04.2

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.

QuantityBound
Distance from s to any vertexnever decreases, at most V − 1
Times one arc is criticalat most V / 2
Augmentationsat most V · E / 2
Work per augmentation (one BFS)O(E)
TotalO(V · E²)
39.04.3

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.

Java
1static int augmentAlongPath(Net g, int[] viaArc, int s, int t) { // viaArc[v]: the arc used to reach v on the s-t path
2 int bottleneck = Integer.MAX_VALUE;
3 for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) bottleneck = Math.min(bottleneck, g.cap[viaArc[v]]);
4 for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) { g.cap[viaArc[v]] -= bottleneck; g.cap[viaArc[v] ^ 1] += bottleneck; }
5 return bottleneck;
6}

Maximum flow: Ford-Fulkerson, Edmonds-Karp, Dinic, push-relabel

Interactive · edit the input

Your input

Try

edge label: flow / capacity · 6 vertices · 9 edges

0

source

1
2
3
4
5

sink

Current path or vertexEdge carrying some flowSaturated edge (flow = capacity)Source side of the minimum cut

residual arcs u→v : remaining capacity

0→1:10
0→2:10
1→2:2
1→3:4
1→4:8
2→4:9
3→5:10
4→3:6
4→5:10
Setup

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.

Step 1 / 11
Java
1static class Net {
2 final int n;
3 final List<Integer>[] adj; // adj[u]: ids of the arcs leaving u
4 final int[] to, cap; // arc k and arc k ^ 1 are each other's reverse
5 int arcs = 0;
6 @SuppressWarnings("unchecked")
7 Net(int n, int maxEdges) {
8 this.n = n; adj = new List[n]; to = new int[2 * maxEdges]; cap = new int[2 * maxEdges];
9 for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
10 }
11 void addEdge(int u, int v, int c) {
12 to[arcs] = v; cap[arcs] = c; adj[u].add(arcs++); // forward arc with the capacity
13 to[arcs] = u; cap[arcs] = 0; adj[v].add(arcs++); // reverse arc: capacity 0 until flow is pushed
14 }
15}
16static int fordFulkerson(Net g, int s, int t) {
17 int flow = 0;
18 while (true) {
19 int pushed = dfs(g, s, t, Integer.MAX_VALUE, new boolean[g.n]); // any augmenting path, found depth-first
20 if (pushed == 0) return flow;
21 flow += pushed;
22 }
23}
24static int dfs(Net g, int u, int t, int f, boolean[] seen) {
25 if (u == t) return f;
26 seen[u] = true;
27 for (int id : g.adj[u]) {
28 int v = g.to[id];
29 if (seen[v] || g.cap[id] == 0) continue;
30 int d = dfs(g, v, t, Math.min(f, g.cap[id]), seen);
31 if (d > 0) { g.cap[id] -= d; g.cap[id ^ 1] += d; return d; } // use d units, open d units of reverse capacity
32 }
33 return 0;
34}
35static int edmondsKarp(Net g, int s, int t) {
36 int flow = 0;
37 while (true) {
38 int[] viaArc = new int[g.n];
39 Arrays.fill(viaArc, -1);
40 Deque<Integer> queue = new ArrayDeque<>(List.of(s));
41 while (!queue.isEmpty() && viaArc[t] == -1) { // BFS: the path with fewest arcs
42 int u = queue.poll();
43 for (int id : g.adj[u]) {
44 int v = g.to[id];
45 if (v != s && viaArc[v] == -1 && g.cap[id] > 0) { viaArc[v] = id; queue.add(v); }
46 }
47 }
48 if (viaArc[t] == -1) return flow;
49 int bottleneck = Integer.MAX_VALUE;
50 for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) bottleneck = Math.min(bottleneck, g.cap[viaArc[v]]);
51 for (int v = t; v != s; v = g.to[viaArc[v] ^ 1]) { g.cap[viaArc[v]] -= bottleneck; g.cap[viaArc[v] ^ 1] += bottleneck; }
52 flow += bottleneck;
53 }
54}
55static int dinic(Net g, int s, int t) {
56 int flow = 0;
57 int[] level = new int[g.n];
58 while (levels(g, s, t, level)) { // new level graph; stop when t is unreachable
59 int[] next = new int[g.n]; // next arc to try at each vertex
60 for (int f; (f = blocking(g, s, t, Integer.MAX_VALUE, level, next)) > 0; ) flow += f;
61 }
62 return flow;
63}
64static boolean levels(Net g, int s, int t, int[] level) {
65 Arrays.fill(level, -1);
66 level[s] = 0;
67 Deque<Integer> queue = new ArrayDeque<>(List.of(s));
68 while (!queue.isEmpty()) {
69 int u = queue.poll();
70 for (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 }
72 return level[t] != -1;
73}
74static int blocking(Net g, int u, int t, int f, int[] level, int[] next) {
75 if (u == t) return f;
76 for (; next[u] < g.adj[u].size(); next[u]++) {
77 int id = g.adj[u].get(next[u]), v = g.to[id];
78 if (g.cap[id] == 0 || level[v] != level[u] + 1) continue; // only arcs one level deeper
79 int d = blocking(g, v, t, Math.min(f, g.cap[id]), level, next);
80 if (d > 0) { g.cap[id] -= d; g.cap[id ^ 1] += d; return d; }
81 }
82 return 0;
83}
84static int pushRelabel(Net g, int s, int t) {
85 int[] height = new int[g.n], excess = new int[g.n];
86 height[s] = g.n; // the source starts as high as possible
87 Deque<Integer> active = new ArrayDeque<>();
88 for (int id : g.adj[s]) { // saturate every arc out of the source
89 int c = g.cap[id], v = g.to[id];
90 if (c == 0) continue;
91 g.cap[id] = 0; g.cap[id ^ 1] += c; excess[v] += c;
92 if (v != t && excess[v] == c) active.add(v);
93 }
94 while (!active.isEmpty()) discharge(g, active.poll(), s, t, height, excess, active);
95 return excess[t];
96}
97static void discharge(Net g, int u, int s, int t, int[] height, int[] excess, Deque<Integer> active) {
98 while (excess[u] > 0) {
99 boolean pushed = false;
100 for (int id : g.adj[u]) {
101 int v = g.to[id];
102 if (g.cap[id] > 0 && height[u] == height[v] + 1) { // push only downhill, by exactly one level
103 int d = Math.min(excess[u], g.cap[id]);
104 g.cap[id] -= d; g.cap[id ^ 1] += d; excess[u] -= d; excess[v] += d;
105 if (v != s && v != t && excess[v] == d) active.add(v);
106 pushed = true;
107 if (excess[u] == 0) break;
108 }
109 }
110 if (!pushed) { // stuck: relabel one above its lowest residual neighbour
111 int lowest = Integer.MAX_VALUE;
112 for (int id : g.adj[u]) if (g.cap[id] > 0) lowest = Math.min(lowest, height[g.to[id]]);
113 height[u] = lowest + 1;
114 }
115 }
116}

Live state

0 augmentations0 phases0 pushes0 relabels

n

6

edges

9

source

0

sink

5

No condition evaluated on this step
Execution trace (dry run table)Show 11 steps
StepLineStateWhat happens
1L16n=6 edges=9 source=0 sink=5Every 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.
2L48path=0→1→3→5 bottleneck=4 flow=0Breadth-first search (fewest arcs) finds the path 0→1→3→5 with 3 arcs. Its smallest residual capacity is 4.
3L51bottleneck=4 flow=4Push 4 along the path. Flow value is now 4.
4L48path=0→1→4→5 bottleneck=6 flow=4Breadth-first search (fewest arcs) finds the path 0→1→4→5 with 3 arcs. Its smallest residual capacity is 6.
5L51bottleneck=6 flow=10Push 6 along the path. Flow value is now 10.
6L48path=0→2→4→5 bottleneck=4 flow=10Breadth-first search (fewest arcs) finds the path 0→2→4→5 with 3 arcs. Its smallest residual capacity is 4.
7L51bottleneck=4 flow=14Push 4 along the path. Flow value is now 14.
8L48path=0→2→4→3→5 bottleneck=5 flow=14Breadth-first search (fewest arcs) finds the path 0→2→4→3→5 with 4 arcs. Its smallest residual capacity is 5.
9L51bottleneck=5 flow=19Push 5 along the path. Flow value is now 19.
10L48flow=19No path with positive residual capacity from 0 to 5 remains: the search ends.
11L16return=19 cut_capacity=19 augmentations=4Maximum 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.