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 from
Network Flow and Matchings nothing can do better. Two
augmenting paths are enough.
The Ford–Fulkerson method from that module 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 Network Flow and Matchings: 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.
A rule that sounds reasonable: take the augmenting path with the most arcs, so the flow spreads over as much of the network as it can. Nothing in Ford–Fulkerson forbids it. (It is not what a plain depth-first search does: finding the path with the most arcs takes an exhaustive search, and a depth-first search that tries each vertex's neighbours in a fixed order finishes this network in at most four augmentations, whichever order it uses. The rule stands for the unlucky choices that the method allows.)
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
The claim about depth-first search, checked for all 72 ways of ordering the neighbours of , and at :
from itertools import permutations, product
def dfs_path(room, s, t, order):
"""The path a depth-first search finds when vertex u tries its neighbours in order[u]."""
seen = {s}
def go(u):
if u == t:
return [t]
for v in order[u]:
if v not in seen and room[u][v] > 0:
seen.add(v)
rest = go(v)
if rest:
return [u] + rest
return go(s)
room0, counts = residual(*rung(1000)), set()
orders = list(product(*(permutations(room0[u]) for u in "sab")))
for choice in orders:
order = dict(zip("sab", choice))
room, n = residual(*rung(1000)), 0
while (path := dfs_path(room, "s", "t", order)):
augment(room, path)
n += 1
counts.add(n)
assert len(orders) == 72 and counts == {2, 4}
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. The bound for integer capacities from Network Flow and Matchings, 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 about 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. In the worst case it is exponential in the number of bits.
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.
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.)
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 .
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, , which is once vertices on no edge are dropped.
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 . The rung network shows
that some sequence of path choices needs 14,000 augmentations, and without the shortest-path
rule nothing in the method rules that sequence out.
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:
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 Amortized Analysis. 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.
Many flow problems have every capacity equal to 1: disjoint routes, and the matchings of Network Flow and Matchings. 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 : then , so the total is less than .
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 every augmenting path has more than vertices. Compare the current matching with a maximum one: the pairs in one but not the other split into vertex-disjoint augmenting paths, one per missing pair, each with more than vertices, so fewer than pairs are missing. That is the Hopcroft–Karp algorithm, , sketched here.
Not by this argument. The step needs every vertex other than and to pass on at most one unit, as in bipartite matching, where each person and each job carries one unit. For general unit-capacity networks the bound proved here is , and is the cited alternative.
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.)
(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 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.
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.
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 with no antiparallel edges (proved) and for bipartite matching (sketched), against Ford–Fulkerson's pseudo-polynomial for integer capacities.
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 (Amortized Analysis): 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?
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.