On this page

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

0/7000 0/9000 0/9000 0/7000 0/1 s a b t flow value = 0
The rung network with k = 1000. Every edge is labelled flow/capacity. Highlighted: s a b t, the path with the most arcs, whose bottleneck is the rung a → b of capacity 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 {s,b} has capacity 7000+7000=14,000, 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.

The naive rule and where it wastes work

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 a→b. Its bottleneck is the rung's capacity, 1. After that push the rung is full, but the residual graph now has a reverse arc b→a 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 s→a and b→t are full.

r=6999 r=1 r=9000 r=9000 r=6999 r=1 r=1 s a b t flow value = 1
The residual graph after the first push (r = room). The longest path is now s b a t: it uses b → a, the reverse of the rung, to take the unit back. Its bottleneck is 1 again.
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 s, a and b at k=1000:

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 7k and 9k the longest-path rule makes exactly 14k augmentations: 14, 140, 1,400 and 14,000 for k=1,10,100,1000. Always taking the path with the fewest arcs makes two, whatever k 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, O(|f*|·E) with |f*| 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.

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.

V is the number of vertices and E the number of edges; the residual graph has at most 2E arcs. A bound is strongly polynomial if it depends on V and E 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 D:

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 s→a is full, and no path of 3 or 4 arcs is left. The third augmentation has 5 arcs:

r=7 r=5 r=9 r=6 r=4 r=2 r=3 r=8 r=4 r=9 r=4 r=4 r=3 s a c b e d t flow value = 7
Network D after the pushes s a d t (4) and s a e t (3), shown as a residual graph. Highlighted: the third shortest path, s c d a e t, 5 arcs. Its arc d → a is the reverse of a → d and has room 4; the bottleneck is 2 (a → e).

s c d a e t uses the arc d→a, the reverse of a→d, to take back 2 of the 4 units that went a→d→t and send them on through e. 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 t" (4+7), 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 df(v) be the BFS distance from s to v in the residual graph Gf, counting only arcs with room.

Lemma (distance monotonicity)

After an Edmonds–Karp augmentation turns f into f′, every vertex v has df′(v)≥df(v). No vertex ever gets closer to s.

Proof. Suppose some vertex gets closer. Among those, take a v whose new distance df′(v) is smallest. Let u be the vertex before v on a shortest s→v path in Gf′, so df′(u)=df′(v)−1. Since u is nearer than v in Gf′, the choice of v means u did not get closer: df′(u)≥df(u). There are two cases for the arc u→v.

  • It already had room in Gf. Then BFS in Gf gives df(v)≤df(u)+1≤df′(u)+1=df′(v), so v did not get closer after all.
  • It had no room in Gf. Then the augmentation created its room, which happens only by pushing along the opposite arc v→u. That arc was on a shortest path of Gf, so df(u)=df(v)+1. Then df′(v)=df′(u)+1≥df(u)+1=df(v)+2: v got farther away, not closer.

Both cases contradict the choice of v. ◻

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 a: at distance 1 at the start and at distance 3 after the second push, once s→a is full and the only way in is scda.

Theorem (Edmonds–Karp). At most VE augmentations, so O(VE2) time in the worst case, whatever the capacities.

Proof. Call a residual arc u→v 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 d(v)=d(u)+1. For it to be critical again it must first regain room, which needs a push along v→u on a shortest path, at a time when d′(u)=d′(v)+1. By the lemma d′(v)≥d(v), so d′(u)≥d(u)+2. Between two critical moments of the same arc, the distance of its tail grows by at least 2. A distance is at most V−2 for the tail of an arc on a path to t, so each arc is critical at most V/2 times. There are at most 2E residual arcs, and every augmentation has a critical arc, so there are at most 2E·V/2=VE augmentations. Each is one BFS, O(V+E), which is O(E) once vertices on no edge are dropped. ◻

Predict: keep everything, but find each path by depth-first search instead of BFS. Does the lemma survive?

No. On D, 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 VE bound are about shortest paths; with "any path" you are back to Ford–Fulkerson's O(|f*|·E). 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.

Dinic: all the shortest paths at once

Edmonds–Karp spends a BFS on every augmentation, though on D 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:

  1. Run BFS from s in Gf. The distance of v is its level.
  2. Keep only the arcs with room that climb exactly one level, u→v with level(v)=level(u)+1. That subgraph is the level graph. Its s→t paths are exactly the shortest augmenting paths.
  3. Push a blocking flow in the level graph: keep pushing along s→t paths of the level graph until every one of them has a full arc.

Stop when BFS no longer reaches t.

On D 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:

7 5 9 4 5 9 4 7 s L0 a L1 c L1 b L2 e L2 d L2 t L3
Phase 1 of Dinic on D: the level graph, with columns at BFS levels 0 to 3 and each arc labelled with its room. The blocking flow pushes 7 and fills the red arcs s → a, a → d and d → t.

Every s→t path in that picture now crosses a red arc, so the phase is over. The next BFS finds t at level 5, not 3:

5 9 4 9 2 4 s L0 c L1 d L2 a L3 b L4 e L4 t L5
Phase 2: t is at level 5. The level graph is s c d a, then b and e, then t; d → a is a reverse arc. The blocking flow pushes 2 and fills a → e.

The arc d→a is the reverse arc from before, and here it climbs a level. The blocking flow pushes 2 and fills a→e. The third phase has t at level 6, a single path, and pushes 2 more:

3 7 2 9 4 2 s L0 c L1 d L2 a L3 b L4 e L5 t L6
Phase 3: t is at level 6 and the level graph is a single path. The blocking flow pushes 2. The next BFS no longer reaches t, and the flow is 7 + 2 + 2 = 11.

Three phases, with (d(s,t),pushed)=(3,7),(5,2),(6,2). 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, d(s,t) 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 s (level 0) to t (level d) therefore has at least d arcs, and exactly d 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 s→t path in the level graph without a full arc. The blocking flow left none. So every path has more than d arcs. ◻

Since d(s,t) grows every phase and is at most V−1, there are at most V−1 phases.

The cost of one phase. Finding the blocking flow is where care is needed. The pushes are found by depth-first searches from s 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 2E per phase. Each successful search fills at least one arc, so there are at most 2E of them, and each walks at most V arcs forward. A phase costs O(VE), and Dinic's algorithm O(V2E) 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.

Unit capacities: 2E phases

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 (u→v and v→u). Then two things improve.

A phase costs O(E). Every arc on a successful search has room 1, so the push fills all of them. The searches' forward steps then total at most 2E, and so do the pointer moves.

Theorem. Dinic's algorithm uses at most 2E phases on such a network, so O(E3/2) time.

Proof. Look at the residual graph after any k phases. By the phase lemma d=d(s,t)≥k+1. For each i=0,1,…,d−1 let Si be the vertices at level at most i. Each Si is a cut. An arc with room that leaves Si can't climb more than one level, so it goes from level exactly i to level i+1. So the d 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), E in all. So some Si has at most E/d arcs leaving it, and the flow still missing is at most E/d≤E/(k+1) by max-flow min-cut in the residual graph. Each further phase adds at least 1, so at most E/(k+1) phases remain, and the total is at most k+E/(k+1). Take k=⌊E⌋: then k+1>E, so the total is less than 2E. ◻

Two further bounds are cited, not proved here. On unit-capacity graphs with no parallel edges the number of phases is also O(V2/3) (Karzanov; Even and Tarjan), so Dinic runs in O(E·min(V2/3,E1/2)). For bipartite matching, where every vertex other than s and t has only one unit to pass on, the same shape of argument gives O(V) phases: after V phases every augmenting path has more than V 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 V vertices, so fewer than V pairs are missing. That is the Hopcroft–Karp algorithm, O(EV), sketched here.

Predict: is O(EV) a bound for every unit-capacity network?

Not by this argument. The V step needs every vertex other than s and t 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 O(E3/2), and O(E·V2/3) is the cited alternative.

Measure the claim

Dinic's algorithm on random unit-capacity networks with E=4V, where s and t are each joined to a fifth of the vertices (seed 451+V):

V E max flow phases bound 2E
100 400 19 6 40
400 1,600 79 8 80
1,600 6,400 309 11 160

On bipartite matching with n vertices per side and 3 random partners each, the maximum matchings have 94, 947 and 4,711 pairs for n=100,1000,5000, 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 V=20,40,80 (64, 242 and 919 edges), against worst-case bounds VE 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 109 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 109, theirs doesn't. (b) Hopcroft–Karp (Dinic on the unit network), O(EV). (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 2E; 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 O(VE2); run Dinic's phases on level graphs and prove there are at most V−1; and prove the 2E phase bound for unit capacities, knowing which bounds are only cited.

Invariant: under shortest-path augmentation no vertex's residual distance from s ever decreases, and in Dinic's algorithm d(s,t) strictly increases with every blocking flow.

Complexity achieved: Edmonds–Karp O(VE2) and Dinic O(V2E), whatever the capacities; O(E3/2) for unit capacities with no antiparallel edges (proved) and O(EV) for bipartite matching (sketched), against Ford–Fulkerson's pseudo-polynomial O(|f*|·E) 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?

Check yourself

  1. Prove that BFS distances in the residual graph never decrease under Edmonds–Karp. Which case of the proof needs the paths to be shortest?
  2. Replace BFS by "any augmenting path" in Edmonds–Karp. Which bound survives? Give a concrete network and a sequence of path choices that makes it slow.
  3. On the rung network with capacities 7k and 9k, 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.

Your turn

Now try it yourself

Write the code, press Run, and scrub through the frames your program draws. The checks tell you when it works.

Loading the lab…

In this lab

  1. Levels and the level graph

    Both algorithms in this module start from the same picture: the residual graph with every vertex labelled by its distance from s, counting only arcs that still have room. That distance is the vertex's level. Build it.

    ROOM is the residual graph of a small network that already carries a flow of 4. ROOM[u][v] is the room on the arc u -> v, and 0 means the arc is full. Some arcs, like m -> k, are reverse arcs: they exist because the flow on k -> m could be taken back.

    First, bfs_levels(room, s): a breadth-first search from s that only follows arcs with room above 0. Work one layer at a time: level 0 is s, level 1 is every new vertex one arc from level 0, and so on. Call show(room, level) after each layer is complete (the starter already calls it once, for s alone). Second, level_arcs(room, level): the sorted list of arcs (u, v) with room above 0 and level[v] == level[u] + 1. That set of arcs is the level graph.

    Each frame draws the reached vertices in columns by level, the level-graph arcs in green with their room, and the other arcs with room between reached vertices dashed. Before you press Run: which level is t on, which vertex is never reached, and does the reverse arc m -> k end up in the level graph?

  2. Trace shortest augmenting paths

    Now the rule that fixes Ford–Fulkerson: always push along a path with the fewest arcs. Answer the four questions before you run anything.

    The network T has the edges s->u 9, s->v 9, u->p 7, u->q 7, v->p 3, p->q 2, p->r 2, p->t 6, q->t 6, r->u 6, r->v 6.

    Write shortest_path(room, s, t): a breadth-first search from s over residual arcs with room above 0, trying each vertex's neighbours in sorted order (sorted(room[u])), and recording the vertex each new vertex was reached from. Return the path from s to t as a list of vertices, or None when t can't be reached. residual, augment, distances (exercise 1's search) and the slow rule longest_path are given.

    The program pushes along your paths on T. It draws one frame before each push, with the path in purple and every vertex's distance from s underneath, and one frame at the end. Watch the distances: the lesson proved that none of them can ever go down. The last frame counts augmentations on a new rung network, s->p 5k, s->q 8k, p->t 8k, q->t 5k and a rung q->p of capacity 1, for the slow rule and for yours.

  3. Dinic's algorithm with current-arc pointers

    Edmonds–Karp runs a whole breadth-first search for every path. Dinic's algorithm runs one per phase: compute the levels, then push along paths of the level graph until every path from s to t in it has a full arc (a blocking flow), and only then search again.

    Write dinic(nodes, edges, s, t), returning the flow's value and the number of phases. residual, bfs_levels and level_arcs are given. After each phase's blocking flow, call show_phase(room, level, arcs, pushed) with that phase's level arcs (computed before pushing) and the amount the phase pushed. It draws the level graph with the arcs the blocking flow filled in red and the rest in green, labelled with their room left.

    The starter pushes a single path per phase, which is just Edmonds–Karp with extra steps. Fix it in two ways. First, keep pushing until no path is left in this phase's level graph. Second, give every vertex a current-arc pointer: an index into its list of arcs that only moves forward during a phase. When an arc turns out useless (full, off the level graph, or leading to a dead end), move the pointer past it, so no search in this phase ever tries it again. Without the pointers the answers are still right, but a check runs a network full of dead ends and counts every read of room[u][v], room[u]'s keys included, through the given residual: it allows 40 reads per edge. (The count is feedback, not a security measure.)

    Before you press Run: how many phases will T take, and at which level will t be in each? Exercise 2's four paths had 3, 3, 4 and 5 arcs.

  4. Measure: phases against 2√E

    The lesson proved two things about Dinic's algorithm when every capacity is 1 and no two edges are antiparallel: a phase costs O(E), and there are at most 2√E phases. Measure both.

    unit_network(V, seed) builds such a network with E = 4V edges, and dinic_counted is Dinic's algorithm with current-arc pointers. First, make it count. Add 1 to stats["phases"] once per phase (after the level search has reached t), and 1 to stats["scans"] every time either search looks at a residual arc, whether or not it uses it. The comments mark the three places. Second, write profile(sizes, seed=2024): for each V it builds unit_network(V, seed + V), runs from 0 to V - 1, and returns (V, E, flow value, phases, scans).

    The program measures V = 100, 400 and 1600 and draws, for each size, two fractions of what the proof allows: phases divided by 2√E, and scans divided by 6E per search (the searches are one per phase plus the last, failed one). Before you run: which of the two bars should stay about level as V grows, and which may shrink?

  5. Mastery challenge: dispatching the evening peak

    It is the evening peak at a delivery platform. About as many couriers are on shift as there are open requests, and pairs lists every (request, courier) pair where the courier can reach the request in time. Each courier takes at most one request, and each request needs at most one courier. The dispatcher wants as many requests served at once as possible, and at peak it has 30,000 requests and 30,000 couriers, with about 3 reachable couriers per request, recomputed every few seconds.

    Write max_dispatch(n_req, n_cour, pairs). It returns a largest possible list of pairs from pairs, using no request and no courier twice, sorted.

    The starter is correct. For each request in turn it searches for a free courier, moving already-assigned requests to other couriers along the way when that frees one up. It is far too slow at peak. A check runs 6,300 requests and 6,300 couriers. There, courier ids are tokens from the dispatch service: they work as set members and dict keys and compare with ==, but they are not numbers, so keep anything indexed by courier in a dict or a set (as the starter's request_of does). Each hash or comparison of a token counts as one lookup, and the check allows 20 lookups per reachable pair. (The count is feedback, not a security measure.)

    Answer the four questions, then write it. The demo draws the plan for 7 requests and 4 couriers.