Polynomial-Time Max Flow
Four vertices, fourteen thousand augmentations
Here is a network with four vertices and five edges. Capacities are in units per second:
s→a 7000, s→b 9000, a→t 9000, b→t 7000, a→b 1
The maximum flow is 14,000, and you can see it without any algorithm. Send 7,000 along
s a t and 7,000 along s b t. The cut has capacity , and
by the max-flow min-cut theorem of module 11 nothing can do better. Two augmenting paths are
enough.
Module 11's Ford–Fulkerson method pushes along any augmenting path until none is left. Which path matters enormously: one plausible rule takes 14,000 augmentations here, and two other rules make the running time depend only on the size of the graph, never on the capacities.
This module assumes module 11: residual graphs, augmenting paths, and the max-flow min-cut theorem. It also uses breadth-first search (BFS), which labels every vertex with its distance in arcs from the start.
The naive rule and where it wastes work
A rule that sounds reasonable: take the augmenting path with the most arcs. A depth-first search tends to find such paths.
On this network the longest path is s a b t, through the rung . Its bottleneck is the
rung's capacity, 1. After that push the rung is full, but the residual graph now has a reverse
arc with room 1, and the longest path is s b a t, which takes the unit back. Its
bottleneck is 1 again. The two paths alternate, each carrying one unit, until and
are full.
def residual(nodes, edges):
"""room[u][v]: residual capacity; every edge also gets its reverse arc."""
room = {u: {} for u in nodes}
for u, v, c in edges:
room[u][v] = room[u].get(v, 0) + c
room[v].setdefault(u, 0)
return room
def augment(room, path):
b = min(room[u][v] for u, v in zip(path, path[1:]))
for u, v in zip(path, path[1:]):
room[u][v] -= b
room[v][u] += b
return b
def paths_with_room(room, s, t):
"""Every simple s-t path whose arcs all have room, in sorted order.
Exhaustive: fine for a handful of vertices, hopeless beyond."""
out = []
def extend(path):
if path[-1] == t:
out.append(list(path))
return
for v in sorted(room[path[-1]]):
if v not in path and room[path[-1]][v] > 0:
path.append(v)
extend(path)
path.pop()
extend([s])
return out
def run(nodes, edges, s, t, choose):
room, value, pushes = residual(nodes, edges), 0, []
while (options := paths_with_room(room, s, t)):
path = choose(options)
pushes.append(("".join(path), augment(room, path)))
value += pushes[-1][1]
return value, pushes, room
longest = lambda options: max(options, key=len)
shortest = lambda options: min(options, key=len)
def rung(k):
return "sabt", [("s", "a", 7 * k), ("s", "b", 9 * k), ("a", "t", 9 * k),
("b", "t", 7 * k), ("a", "b", 1)]
for k in (1, 10, 100, 1000):
value, pushes, _ = run(*rung(k), "s", "t", longest)
assert value == 14 * k and len(pushes) == 14 * k
assert pushes[:2] == [("sabt", 1), ("sbat", 1)]
value, pushes, _ = run(*rung(k), "s", "t", shortest)
assert value == 14 * k and len(pushes) == 2
With capacities and the longest-path rule makes exactly augmentations: 14, 140, 1,400 and 14,000 for . Always taking the path with the fewest arcs makes two, whatever is.
The waste is plain to see: every long path squeezes through the rung, so every push moves one unit. Module 11's bound, with the maximum flow's value, is honest here and useless. The input is five numbers of at most 14 bits each. Multiply the capacities by 1,000 and the input grows by 10 bits per number, while the work grows a thousandfold. A running time that grows with the values in the input, rather than with its length, is called pseudo-polynomial. It is exponential in the number of bits.
The model: what we count
We count three things. An augmentation is one push along one path. A phase (in Dinic's algorithm, below) is one BFS followed by many pushes. An arc scan is one look at one residual arc, and it is the unit of time: a BFS or a DFS costs one scan per arc it looks at.
is the number of vertices and the number of edges; the residual graph has at most arcs. A bound is strongly polynomial if it depends on and alone and not on the capacities. Everything proved below is a worst-case bound in that sense.
Shortest augmenting paths
Edmonds–Karp is Ford–Fulkerson with one rule: always augment along a path with the fewest
arcs, found by BFS. On the rung network that gives s a t and s b t, and it stops.
A single example proves nothing, so here is a larger one, the network :
s→a 7, s→c 5, a→b 9, a→c 6, a→d 4, a→e 5, b→c 8, b→e 4, c→d 9, d→t 4, e→t 7
Ties between equally short paths are broken by trying neighbours in alphabetical order. The
first two augmentations are s a d t (bottleneck 4) and s a e t (bottleneck 3), both with
3 arcs. After them is full, and no path of 3 or 4 arcs is left. The third
augmentation has 5 arcs:
s c d a e t uses the arc , the reverse of , to take back 2 of the 4 units
that went and send them on through . The fourth augmentation, s c d a b e t,
has 6 arcs. The flow is then 11, which equals the capacity of the cut "everything but "
(), so it is maximum.
DN = "sabcdet"
DE = [("s", "a", 7), ("s", "c", 5), ("a", "b", 9), ("a", "c", 6), ("a", "d", 4),
("a", "e", 5), ("b", "c", 8), ("b", "e", 4), ("c", "d", 9), ("d", "t", 4),
("e", "t", 7)]
value, pushes, room = run(DN, DE, "s", "t", shortest)
assert pushes == [("sadt", 4), ("saet", 3), ("scdaet", 2), ("scdabet", 2)]
assert value == 11 == 4 + 7
The lengths went 3, 3, 5, 6. They never went down, and that is no accident.
(The lesson's code lists every path and picks the shortest. That is a stand-in, fine for seven vertices. The real algorithm finds the path with one BFS, and you write it in the lab.)
The invariant: distances never shrink
Let be the BFS distance from to in the residual graph , counting only arcs with room.
Lemma (distance monotonicity)
After an Edmonds–Karp augmentation turns into , every vertex has . No vertex ever gets closer to .
Proof. Suppose some vertex gets closer. Among those, take a whose new distance is smallest. Let be the vertex before on a shortest path in , so . Since is nearer than in , the choice of means did not get closer: . There are two cases for the arc .
- It already had room in . Then BFS in gives , so did not get closer after all.
- It had no room in . Then the augmentation created its room, which happens only by pushing along the opposite arc . That arc was on a shortest path of , so . Then : got farther away, not closer.
Both cases contradict the choice of .
The step not to skip is the second case. New arcs appear only as reverses of arcs just used, and a shortest path climbs one level per arc, so every new arc points one level down. An arc that points down can't shorten anything.
def dist_from(room, s):
"""Fewest arcs from s to each vertex, over arcs with room (stand-in for BFS)."""
best = {s: 0}
def extend(path):
for v in room[path[-1]]:
if v not in path and room[path[-1]][v] > 0:
if len(path) < best.get(v, len(path) + 1):
best[v] = len(path)
extend(path + [v])
extend([s])
return best
room, seen = residual(DN, DE), []
while (options := paths_with_room(room, "s", "t")):
seen.append(dist_from(room, "s"))
augment(room, shortest(options))
seen.append(dist_from(room, "s"))
for before, after in zip(seen, seen[1:]):
assert all(after.get(v, 99) >= before[v] for v in before)
assert seen[0] == {"s": 0, "a": 1, "c": 1, "b": 2, "d": 2, "e": 2, "t": 3}
assert seen[2] == {"s": 0, "c": 1, "d": 2, "a": 3, "b": 4, "e": 4, "t": 5}
Look at : at distance 1 at the start and at distance 3 after the second push, once is full and the only way in is .
Theorem (Edmonds–Karp). At most augmentations, so time in the worst case, whatever the capacities.
Proof. Call a residual arc critical in an augmentation if it is a bottleneck; the push removes all its room. At that moment it lies on a shortest path, so . For it to be critical again it must first regain room, which needs a push along on a shortest path, at a time when . By the lemma , so . Between two critical moments of the same arc, the distance of its tail grows by at least 2. A distance is at most for the tail of an arc on a path to , so each arc is critical at most times. There are at most residual arcs, and every augmentation has a critical arc, so there are at most augmentations. Each is one BFS, .
Predict: keep everything, but find each path by depth-first search instead of BFS. Does the lemma survive?
No. On , a depth-first search trying neighbours alphabetically first finds s a b c d t, 5
arcs, although s a d t has 3. The lemma and the bound are about shortest paths;
with "any path" you are back to Ford–Fulkerson's , and the rung network shows
that a depth-first search really can pick those long paths.
Dinic: all the shortest paths at once
Edmonds–Karp spends a BFS on every augmentation, though on the first two found the same distances. Dinic's algorithm reuses one BFS for every path of the current length. Each phase does three things:
- Run BFS from in . The distance of is its level.
- Keep only the arcs with room that climb exactly one level, with . That subgraph is the level graph. Its paths are exactly the shortest augmenting paths.
- Push a blocking flow in the level graph: keep pushing along paths of the level graph until every one of them has a full arc.
Stop when BFS no longer reaches .
On the first level graph has four columns. Its blocking flow pushes 4 along s a d t and 3
along s a e t. Red arcs are the ones the blocking flow filled, green arcs still have room, and
each number is the arc's room at the start of the phase:
Every path in that picture now crosses a red arc, so the phase is over. The next BFS finds at level 5, not 3:
The arc is the reverse arc from before, and here it climbs a level. The blocking flow pushes 2 and fills . The third phase has at level 6, a single path, and pushes 2 more:
Three phases, with . Here they push exactly Edmonds–Karp's four paths, grouped by length.
by_length = {}
for path, pushed in run(DN, DE, "s", "t", shortest)[1]:
by_length[len(path) - 1] = by_length.get(len(path) - 1, 0) + pushed
assert list(by_length.items()) == [(3, 7), (5, 2), (6, 2)]
Lemma (a phase lengthens the shortest path). After a blocking flow, is strictly larger.
Proof. Use the phase's levels . Every arc with room before the phase goes up at most one level, because BFS would otherwise have given its head a smaller level. The pushes create new arcs only as reverses of level-graph arcs, and those go one level down. So after the phase, still no arc climbs more than one level. A path from (level 0) to (level ) therefore has at least arcs, and exactly only if every arc climbs one level. Such arcs are old level-graph arcs with room left, and a path made of them would be an path in the level graph without a full arc. The blocking flow left none. So every path has more than arcs.
Since grows every phase and is at most , there are at most phases.
The cost of one phase. Finding the blocking flow is where care is needed. The pushes are found by depth-first searches from in the level graph. Each vertex keeps a current-arc pointer into its list of arcs. When an arc turns out useless (it is full, or the search behind it reached a dead end), the pointer moves past it and never comes back in this phase. The arc stays useless for the rest of the phase, because pushes only fill level-graph arcs and never add room to them.
So pointer moves total at most per phase. Each successful search fills at least one arc, so there are at most of them, and each walks at most arcs forward. A phase costs , and Dinic's algorithm in the worst case. This is sketched, not fully proved: the pointer count is an amortized argument of the kind in module 07. Charge each pointer move to the arc it skips, and each arc can be skipped once.
Without the pointers the answers are still right, but every search can walk into the same dead ends again.
Unit capacities: phases
Many flow problems have every capacity equal to 1: disjoint routes, and matchings in module 11. Assume also that no two edges are antiparallel ( and ). Then two things improve.
A phase costs . Every arc on a successful search has room 1, so the push fills all of them. The searches' forward steps then total at most , and so do the pointer moves.
Theorem. Dinic's algorithm uses at most phases on such a network, so time.
Proof. Look at the residual graph after any phases. By the phase lemma . For each let be the vertices at level at most . Each is a cut. An arc with room that leaves can't climb more than one level, so it goes from level exactly to level . So the cuts use disjoint sets of arcs. With unit capacities and no antiparallel edges, each edge gives exactly one arc with room (forward if unused, reverse if used), in all. So some has at most arcs leaving it, and the flow still missing is at most by max-flow min-cut in the residual graph. Each further phase adds at least 1, so at most phases remain, and the total is at most . Take .
Two further bounds are cited, not proved here. On unit-capacity graphs with no parallel edges the number of phases is also (Karzanov; Even and Tarjan), so Dinic runs in . For bipartite matching, where every vertex other than and has only one unit to pass on, the same shape of argument gives phases: after phases the remaining augmenting paths are vertex-disjoint and each has more than vertices, so fewer than of them are left. That is the Hopcroft–Karp algorithm, , sketched here.
Predict: is a bound for every unit-capacity network?
No. needs unit capacity on the vertices too, as in bipartite matching, where each person and each job passes on one unit. For general unit-capacity networks the bound proved here is , and is the cited alternative.
Measure the claim
Dinic's algorithm on random unit-capacity networks with , where and are each joined to a fifth of the vertices (seed ):
| max flow | phases | bound | ||
|---|---|---|---|---|
| 100 | 400 | 19 | 6 | 40 |
| 400 | 1,600 | 79 | 8 | 80 |
| 1,600 | 6,400 | 309 | 11 | 160 |
On bipartite matching with vertices per side and 3 random partners each, the maximum matchings have 94, 947 and 4,711 pairs for , found in 4, 7 and 9 phases. And Edmonds–Karp on random networks with capacities up to 100 needed 7, 8 and 29 augmentations for (64, 242 and 919 edges), against worst-case bounds of 1,280, 9,680 and 73,520.
These are measurements on these instances, not theorems: random networks are easy, and the bounds are for the worst case. (The course's verification script computes these numbers.)
Which algorithm when?
Predict: (a) capacities up to on 50 vertices; (b) matching 10,000 people to 10,000 jobs; (c) six edges with capacities at most 3. Which algorithm for each?
(a) Edmonds–Karp or Dinic: Ford–Fulkerson's bound grows with , theirs doesn't. (b) Hopcroft–Karp (Dinic on the unit network), . (c) Anything: the flow is at most 18, so even Ford–Fulkerson makes at most 18 augmentations.
Faster maximum-flow algorithms exist: push–relabel methods, and recent algorithms that run in almost linear time. They are beyond this module.
A problem that looks different
A company is choosing research projects. Each project earns a known profit, but needs some instruments, and each instrument costs money once, however many projects share it. Profits and costs run into the millions. Which projects should it choose? Somewhere in that question is a cut, and once you find it, you will want an algorithm whose running time doesn't depend on millions. The lab's last problem is a different one.
Practise
In the lab you build BFS levels and the level graph and watch every arc climb exactly one level; predict, then trace, Edmonds–Karp on a new network while the distances refuse to shrink, and count how badly the long-path rule does on a new rung network; write Dinic's algorithm with current-arc pointers, drawing a level graph per phase; count phases and arc scans on unit-capacity networks against ; and solve a dispatching problem that doesn't say what it is.
Recap
You can now: build a network on which a plausible path rule needs a number of augmentations proportional to the capacities; prove that shortest augmenting paths never get shorter and derive Edmonds–Karp's ; run Dinic's phases on level graphs and prove there are at most ; and prove the phase bound for unit capacities, knowing which bounds are only cited.
Invariant: under shortest-path augmentation no vertex's residual distance from ever decreases, and in Dinic's algorithm strictly increases with every blocking flow.
Complexity achieved: Edmonds–Karp and Dinic , whatever the capacities; for unit capacities (proved) and for bipartite matching (sketched), against Ford–Fulkerson's pseudo-polynomial .
Failure mode: a "Dinic" whose searches don't keep a current-arc pointer. It returns correct answers and re-explores the same dead ends in every search. A close second: finding paths by DFS and calling it Edmonds–Karp.
In real software: SciPy's scipy.sparse.csgraph.maximum_flow uses Dinic's algorithm by
default (method='dinic') and offers method='edmonds_karp'; its maximum_bipartite_matching
implements Hopcroft–Karp. NetworkX provides dinitz and edmonds_karp flow functions.
Retrieval (module 07): one Dinic phase may run hundreds of depth-first searches. Why do the pointer moves in a phase still total at most the number of arcs?
Check yourself
- Prove that BFS distances in the residual graph never decrease under Edmonds–Karp. Which case of the proof needs the paths to be shortest?
- Replace BFS by "any depth-first path" in Edmonds–Karp. Which bound survives? Give a concrete network.
- On the rung network with capacities and , how many BFS runs do Edmonds–Karp and Dinic's algorithm each make, and why does the rung never enter Dinic's level graph?
Topic list modeled on the public schedule of CMU 15-451/651 Algorithm Design and Analysis (Fall 2025). All lecture text, proofs, examples, code, and exercises are original to SciMigo; no CMU course materials are reproduced. This course is independent and not affiliated with or endorsed by Carnegie Mellon University.