DSA Graphs
Edmonds-Karp Algorithm
Dual conflict: Ford-Fulkerson is correct; DFS path choice can take forever on large integer capacities. Fix the search: always augment a found by BFS.
Saturating a forces residual distances to grow, giving a of O(VE²).
Algorithm journey
BFS in residual graph
Parent pointers build the path.
Bottleneck Δ
Augment; update reverse edges.
At least one edge saturates.
No BFS path to t
Flow is maximum; read the cut.
What actually happens
Naive idea: keep DFS Ford-Fulkerson. Breaks when each augmentation adds only 1 on a network with huge |f*|.
Better idea: BFS shortest paths. Distances from s in the residual graph are nondecreasing; only O(VE) augmentations occur.
The improved design still fails as a practical choice on large sparse graphs where Dinic's blocking flows do less repeated BFS work per unit progress.
Invariant
Each augmentation uses a currently shortest residual s–t path. Shortest-path distances from s never decrease between augmentations.
How to choose Edmonds-Karp
1. Need a simple poly algorithm? Edmonds-Karp is the clean Ford-Fulkerson specialization.
2. Sparse contest graphs? Prefer Dinic.
3. Dense practical nets? Consider push-relabel.
Quantitative check
O(VE²): V=100, E=500 ⇒ bound ~2.5×10⁷. Comfortable. V=2000, E=8000 ⇒ ~1.3×10¹¹ in the worst-case expression: switch algorithms.
Interactive demo
🎯 Key Insight: Why BFS?
Edmonds-Karp uses BFS to find the shortest augmenting path (in terms of number of edges). This guarantees polynomial time complexity O(V E²), independent of edge capacities! The shortest path length is non-decreasing, ensuring at most O(V E) iterations.
Invariant: shortest residual distance
- 1. BFS labels are exact: dist[v] is the fewest residual edges from the source.
- 2. Distances never decrease: augmentation can add a reverse edge, but that edge points to the previous BFS layer and cannot create a shorter source path.
- 3. Charge an augmentation: at least one edge on its path becomes saturated; call it a critical edge.
- 4. Reuse costs two levels: before the same directed edge can be critical again, its tail's BFS distance must increase by at least 2.
- 5. Count: each of O(E) residual edges is critical O(V) times, so there are O(VE) augmentations; each BFS/update costs O(E), yielding O(VE²).
💡 Why BFS? The Magic of Shortest Paths
Using BFS to find augmenting paths ensures we always find the shortest path (in terms of number of edges) from source to sink. This seemingly simple choice has profound implications:
✓ Guaranteed Termination
The shortest path length is non-decreasing, ensuring the algorithm terminates in at most O(V E) iterations.
✓ Polynomial Time
Unlike DFS-based Ford-Fulkerson, Edmonds-Karp runs in O(V E²) time regardless of edge capacities!
✓ No Pathological Cases
Avoids the exponential-time worst cases that can occur with poor path choices in basic Ford-Fulkerson.
✓ Predictable Performance
Performance doesn't depend on the maximum flow value, making it reliable for large capacities.
🔍 Did You Know? Polynomial Time Guarantee
Before Edmonds-Karp (1972), Ford-Fulkerson's time complexity depended on the maximum flow value, which could be arbitrarily large! Edmonds and Karp proved that using BFS gives a strongly polynomial time complexity of O(V E²), independent of edge capacities. This was a major breakthrough in network flow algorithms!
📝 Algorithm Steps
Initialize
Set flow to 0 on all edges. Create residual graph with original capacities.
BFS for Shortest Path
Use BFS to find the shortest augmenting path from source to sink in the residual graph. Track parent pointers to reconstruct the path.
Calculate Bottleneck
Find the minimum residual capacity along the path found by BFS.
Augment Flow
Increase flow along the path by the bottleneck value. Update residual graph: decrease forward capacities, increase backward capacities.
Repeat
Repeat steps 2-4 until BFS cannot find any path from source to sink.
💻 Complete edge-list implementation
Python
from collections import deque
class Edge:
def __init__(self, to: int, rev: int, residual: int) -> None:
self.to, self.rev, self.residual = to, rev, residual
class EdmondsKarp:
def __init__(self, n: int) -> None:
if n <= 0:
raise ValueError("n must be positive")
self.graph = [[] for _ in range(n)]
def add_edge(self, u: int, v: int, capacity: int) -> None:
if not (0 <= u < len(self.graph) and 0 <= v < len(self.graph)):
raise IndexError("vertex out of range")
if u == v:
raise ValueError("self-loops are not supported")
if capacity < 0:
raise ValueError("capacity must be non-negative")
forward = Edge(v, len(self.graph[v]), capacity)
reverse = Edge(u, len(self.graph[u]), 0)
self.graph[u].append(forward)
self.graph[v].append(reverse)
def _bfs(self, source: int, sink: int):
parent = [None] * len(self.graph) # (previous vertex, edge index)
parent[source] = (-1, -1)
queue = deque([source])
while queue:
u = queue.popleft()
for edge_index, edge in enumerate(self.graph[u]):
if edge.residual == 0 or parent[edge.to] is not None:
continue
parent[edge.to] = (u, edge_index)
if edge.to == sink:
return parent
queue.append(edge.to)
return None
def max_flow(self, source: int, sink: int) -> int:
if not (0 <= source < len(self.graph) and 0 <= sink < len(self.graph)):
raise IndexError("vertex out of range")
if source == sink:
raise ValueError("source and sink must differ")
total = 0
while (parent := self._bfs(source, sink)) is not None:
bottleneck = None
v = sink
while v != source:
u, edge_index = parent[v]
residual = self.graph[u][edge_index].residual
bottleneck = residual if bottleneck is None else min(bottleneck, residual)
v = u
assert bottleneck is not None
v = sink
while v != source:
u, edge_index = parent[v]
edge = self.graph[u][edge_index]
edge.residual -= bottleneck
self.graph[edge.to][edge.rev].residual += bottleneck
v = u
total += bottleneck
return total
flow = EdmondsKarp(6)
for edge in [
(0,1,16), (0,2,13), (1,2,10), (2,1,4), (1,3,12),
(2,4,14), (3,2,9), (4,3,7), (3,5,20), (4,5,4),
]:
flow.add_edge(*edge)
assert flow.max_flow(0, 5) == 23Java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class EdmondsKarp {
static final class Edge {
final int to, rev;
long residual;
Edge(int to, int rev, long residual) {
this.to = to; this.rev = rev; this.residual = residual;
}
}
private final List<List<Edge>> graph;
EdmondsKarp(int n) {
if (n <= 0) throw new IllegalArgumentException("n must be positive");
graph = new ArrayList<>(n);
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
}
void addEdge(int u, int v, long capacity) {
if (u < 0 || u >= graph.size() || v < 0 || v >= graph.size()) {
throw new IndexOutOfBoundsException("vertex out of range");
}
if (u == v) throw new IllegalArgumentException("self-loop");
if (capacity < 0) throw new IllegalArgumentException("negative capacity");
Edge forward = new Edge(v, graph.get(v).size(), capacity);
Edge reverse = new Edge(u, graph.get(u).size(), 0L);
graph.get(u).add(forward);
graph.get(v).add(reverse);
}
private boolean bfs(int source, int sink, int[] parentV, int[] parentE) {
Arrays.fill(parentV, -1);
parentV[source] = source;
ArrayDeque<Integer> queue = new ArrayDeque<>();
queue.add(source);
while (!queue.isEmpty()) {
int u = queue.remove();
for (int i = 0; i < graph.get(u).size(); i++) {
Edge edge = graph.get(u).get(i);
if (edge.residual == 0 || parentV[edge.to] != -1) continue;
parentV[edge.to] = u;
parentE[edge.to] = i;
if (edge.to == sink) return true;
queue.add(edge.to);
}
}
return false;
}
long maxFlow(int source, int sink) {
if (source < 0 || source >= graph.size() || sink < 0 || sink >= graph.size()) {
throw new IndexOutOfBoundsException("vertex out of range");
}
if (source == sink) throw new IllegalArgumentException("source == sink");
int[] parentV = new int[graph.size()];
int[] parentE = new int[graph.size()];
long total = 0L;
while (bfs(source, sink, parentV, parentE)) {
long bottleneck = Long.MAX_VALUE;
for (int v = sink; v != source; v = parentV[v]) {
bottleneck = Math.min(bottleneck,
graph.get(parentV[v]).get(parentE[v]).residual);
}
for (int v = sink; v != source; v = parentV[v]) {
Edge edge = graph.get(parentV[v]).get(parentE[v]);
edge.residual -= bottleneck;
graph.get(edge.to).get(edge.rev).residual += bottleneck;
}
total = Math.addExact(total, bottleneck);
}
return total;
}
public static void main(String[] args) {
EdmondsKarp flow = new EdmondsKarp(6);
long[][] edges = {
{0,1,16}, {0,2,13}, {1,2,10}, {2,1,4}, {1,3,12},
{2,4,14}, {3,2,9}, {4,3,7}, {3,5,20}, {4,5,4}
};
for (long[] edge : edges) flow.addEdge((int) edge[0], (int) edge[1], edge[2]);
if (flow.maxFlow(0, 5) != 23L) throw new AssertionError();
}
}🎯 Key Insight: BFS Level Property
The key to Edmonds-Karp's polynomial time is that BFS finds paths in increasing order of length. Each augmentation saturates a critical edge. Before that directed edge can be critical again, the BFS distance to its tail increases by at least two. A vertex distance can increase only O(V) times, so O(E) edges account for at most O(V E) augmentations.
⚡ Complexity Analysis
Time Complexity
- •O(V E²) overall
At most O(V E) augmentations, each BFS is O(E)
- •Independent of capacities
Works well even with very large edge weights
- •Strongly polynomial
Depends only on V and E, not on numbers
Space Complexity
- •O(V + E)
Paired adjacency-list residual graph and BFS arrays
- •O(V) auxiliary
For BFS queue, visited array, parent array
🔄 Edmonds-Karp vs Ford-Fulkerson
| Aspect | Edmonds-Karp | Ford-Fulkerson (DFS) |
|---|---|---|
| Path Finding | BFS (shortest path) | DFS (any path) |
| Time Complexity | O(V E²) | O(E × f) |
| Polynomial Time | ✓ Yes | ✗ No |
| Depends on Capacities | ✗ No | ✓ Yes |
| Worst Case | Predictable | Can be exponential |
| Practical Use | Clear polynomial baseline | Only for small integral-flow cases |
✅ Pros & ❌ Cons
✅ Advantages
- ✓Guaranteed polynomial time O(V E²)
- ✓Performance independent of edge capacities
- ✓Simple to implement (just BFS + flow update)
- ✓No pathological worst cases
- ✓Works well for general-purpose max flow problems
❌ Limitations
- ✗Slower than modern algorithms (Dinic's, Push-Relabel)
- ✗O(V E²) can be slow for very large dense graphs
- ✗Not optimal for unit capacity networks
- ✗Often slower than Dinic or tuned Push-Relabel on large inputs
💼 Real World Application: Data Center Networks
A bandwidth graph is a natural max-flow model: links are capacities, routers are vertices, and a minimum cut exposes a throughput bottleneck. Edmonds-Karp is valuable as a small-instance solver and correctness oracle because its behavior is predictable and easy to inspect; performance-critical systems typically benchmark more advanced implementations.
💡 Pro Tip: When to Use Edmonds-Karp
Use Edmonds-Karp when implementation clarity, a proof-friendly baseline, or a differential-test oracle matters. For larger sparse graphs, Dinic is usually the stronger default; for large dense workloads, benchmark Push-Relabel. The best choice depends on topology, not only vertex count.
Cut extraction and production checks
Return a certificate
After the last failed BFS, run one full BFS from the source. Reachable vertices form S; every original edge from S to its complement belongs to a minimum cut. Return this partition with the flow when callers need explainability.
Cases that break naive code
- • source == sink can create an infinite loop; reject it.
- • Reject self-loops or handle their reverse indices specially; they cannot improve s–t flow.
- • Matrix updates conflate real antiparallel edges with residual reverse edges.
- • Parallel edges need distinct paired reverse indices.
- • Use Java long and checked addition for total flow.
- • An unreachable sink is a valid answer: flow 0.
- • The residual object is one-shot; rebuild it for a second query.
Test oracle: check capacity bounds, conservation at every internal vertex, source net outflow = sink net inflow, and final flow value = extracted cut capacity. Differential-test random tiny graphs against a slower solver.
Short takeaway
Edmonds-Karp is Ford-Fulkerson with BFS shortest augmenting paths, giving an O(VE²) bound independent of capacity magnitudes.
Exercises
- Why does saturating a critical edge on a shortest path prevent that path length from decreasing later?
- Compare DFS Ford-Fulkerson vs Edmonds-Karp on a classic dual-path unit-augmentation graph.
- When would you still prefer Dinic over Edmonds-Karp at V≈500?
Primary sources
- Edmonds, J. & Karp, R. M. (1972): Theoretical improvements in algorithmic efficiency for network flow problems.
- Cormen et al., CLRS: Edmonds-Karp analysis.
- Ford & Fulkerson: underlying augmenting-path method.