On this page

Minimum-Cost Flow

The question

A courier firm moves parcels from its depot s to a hub t through three relay points a, b and c. Each one-way link has a capacity (parcels per hour) and a cost per parcel:

s→a 4/1, s→b 1/6, a→b 4/2, a→t 4/7, b→c 1/3, b→t 2/2, c→a 3/6 (capacity/cost)

4/1 1/6 4/2 4/7 1/3 2/2 3/6 s a b c t
The courier network used throughout this lesson. Each link is labelled capacity/cost per parcel.

The firm must move 5 parcels an hour. That is also the most the network can carry: only 4+1=5 can leave s. There are many ways to route five parcels, and they don't cost the same. Which is cheapest, and how would you convince an auditor that nothing cheaper exists?

This is minimum-cost flow: a network with capacities and a cost per unit on every edge, a demand d, and the task of sending d units from s to t at the least total cost ∑f(u,v)a(u,v), or reporting that d units don't fit. The module assumes Network Flow and Matchings (flows, residual graphs, max-flow min-cut) and Dynamic Programming II (Bellman–Ford and Dijkstra).

A maximum flow is not the answer

The obvious approach is to run a maximum-flow algorithm and stop. It returns some flow of value 5, but a max-flow algorithm never reads the costs, so nothing stops it from returning this one: 4 parcels on s→a→t, and 1 parcel that goes s→b→c→a→t, taking a detour through c that doubles back to a. It is a valid flow of value 5, and it costs 51.

Brute force would find the cheapest: try every integer amount on every edge. Here that is 5·2·5·5·2·3·4=6000 vectors, and the count is exponential in the number of edges.

from itertools import product

EDGES = [("s", "a", 4, 1), ("s", "b", 1, 6), ("a", "b", 4, 2), ("a", "t", 4, 7),
         ("b", "c", 1, 3), ("b", "t", 2, 2), ("c", "a", 3, 6)]   # (u, v, capacity, cost)
COST = {(u, v): c for u, v, _, c in EDGES}

def flow_cost(flow):
    return sum(f * e[3] for e, f in zip(EDGES, flow))

def cheapest_by_brute_force(d):
    """Try every integer flow vector; return the least cost of a flow of value d."""
    best, tried = None, 0
    for f in product(*[range(cap + 1) for _, _, cap, _ in EDGES]):
        tried += 1
        net = {v: 0 for v in "sabct"}
        for (u, v, _, _), x in zip(EDGES, f):
            net[u] -= x
            net[v] += x
        if net["t"] == d and net["a"] == net["b"] == net["c"] == 0:
            c = flow_cost(f)
            best = c if best is None or c < best else best
    return best, tried

WASTEFUL = [4, 1, 1, 4, 1, 1, 1]      # one parcel detours b -> c -> a
assert flow_cost(WASTEFUL) == 51
assert cheapest_by_brute_force(5) == (37, 6000)
assert cheapest_by_brute_force(6)[0] is None       # six parcels don't fit

The cheapest routing costs 37, and the wasteful one pays 14 more. The waste is in the detour: the parcel that goes b→c→a pays 3+6 to reach a, then 7 more on a→t, when b had a direct link to t at cost 2 with room to spare.

The model: residual arcs have prices

The residual graph from Network Flow and Matchings says how a flow can change. For every edge u→v carrying f(u,v) it has a forward arc u→v with room c(u,v)−f(u,v) and a reverse arc v→u with room f(u,v). Now the arcs get prices too:

  • the forward arc costs a(u,v) per unit, since sending more pays the edge's price;
  • the reverse arc v→u costs −a(u,v) per unit: taking a unit back refunds what it cost.

So residual graphs have negative arcs even when every edge cost is positive. Pushing one unit around a cycle of residual arcs keeps every vertex balanced and the value unchanged, and it changes the total cost by exactly the cycle's cost. A cycle with negative cost is money left on the table.

Under the wasteful flow the reverse arcs are a→s (−1), b→s (−6), b→a (−2), t→a (−7), c→b (−3), t→b (−2) and a→c (−6), and three simple cycles are negative:

  • a→c→b→t→a costs −6−3+2−7=−14;
  • a→c→b→a costs −6−3−2=−11;
  • a→b→t→a costs 2+2−7=−3.

The block below finds them by trying every simple cycle of residual arcs that have room:

residual = {}                                # (u, v) -> price of a residual arc with room
for (u, v, cap, cost), f in zip(EDGES, WASTEFUL):
    if f < cap:
        residual[(u, v)] = cost
    if f > 0:
        residual[(v, u)] = -cost             # the refund

def negative_cycles():
    """Every simple residual cycle with negative cost, each listed once from its first vertex."""
    found = []
    def extend(path, cost):
        for (u, v), price in residual.items():
            if u == path[-1] and v == path[0] and cost + price < 0:
                found.append((" → ".join(path + [v]), cost + price))
            elif u == path[-1] and v > path[0] and v not in path:
                extend(path + [v], cost + price)
    for start in "abcst":
        extend([start], 0)
    return sorted(found, key=lambda cycle: cycle[1])

assert negative_cycles() == [("a → c → b → t → a", -14), ("a → c → b → a", -11), ("a → b → t → a", -3)]

The first one reads as a plan: take the detouring parcel back off c→a and b→c, send it b→t instead, and remove one parcel from a→t.

4/-1 1/-6 3/2 1/-2 4/-7 1/-3 1/2 1/-2 2/6 1/-6 s a b c t
The residual graph of the wasteful flow (cost 51), each arc labelled room/cost. In purple, the cycle a → c → b → t → a: its costs add up to −6 − 3 + 2 − 7 = −14, and every arc on it has room 1.
Predict: someone prices reverse arcs at +a(u,v) instead, as if sending back cost as much as sending forward. What goes wrong?

Every residual arc is then positive, so no cycle is ever negative, and the wasteful flow of 51 would pass as optimal. Pushing a unit back undoes a unit that was paid for, so its price must be refunded. The code runs either way, and only the answer is wrong.

When is a flow cheapest?

Theorem (optimality criterion)

A feasible flow f of value d costs the least among all feasible flows of value d if and only if its residual graph Gf has no cycle of negative cost.

Proof. If Gf has a negative cycle, push its bottleneck b>0 (the least room on it) around it. Every vertex on the cycle gains and loses b, so conservation and the value hold, the rooms keep the flow within capacity, and the cost drops by b times the cycle's cost. So f was not the cheapest.

Conversely, suppose some flow f* of the same value is cheaper. The difference f*−f, read edge by edge (a positive difference uses the forward arc, a negative one the reverse arc), is a circulation in Gf: at every vertex, including s and t, as much enters as leaves, because both flows have the same value. A circulation splits into cycles of Gf. This is the standard decomposition lemma, which we use without proof: repeatedly follow arcs carrying some of the circulation until a vertex repeats, and remove that cycle. The costs of those cycles add up to cost(f*)−cost(f)<0, so at least one of them is negative. ◻

The theorem gives the auditor a certificate. To prove a flow is cheapest you don't compare it with the 6000 vectors: you show that its residual graph has no negative cycle, which Bellman–Ford from a virtual source (joined to every vertex at cost 0) checks in O(VE).

Canceling cycles

The theorem is also an algorithm. Start from any flow of value d (a max-flow algorithm gives one), and while the residual graph has a negative cycle, push its bottleneck around it. On the courier network one cancellation of the −14 cycle, with bottleneck 1, turns 51 into 37:

a, b, c, t = "a", "b", "c", "t"
cycle = [(a, c, -COST[(c, a)]), (c, b, -COST[(b, c)]), (b, t, COST[(b, t)]), (t, a, -COST[(a, t)])]
assert sum(price for _, _, price in cycle) == -14
assert -COST[(c, a)] - COST[(b, c)] - COST[(a, b)] == -11     # the second negative cycle

after = list(WASTEFUL)
after[6] -= 1          # a -> c takes a unit back from c -> a
after[4] -= 1          # c -> b takes a unit back from b -> c
after[5] += 1          # b -> t sends one more
after[3] -= 1          # t -> a takes a unit back from a -> t
assert after == [4, 1, 1, 3, 0, 2, 0] and flow_cost(after) == 51 - 14 == 37

With integer capacities and costs each cancellation lowers the cost by at least 1, so the loop ends. How many cancellations it takes depends on the numbers, not only on the size of the network. The lab has you write the loop and watch the cost fall frame by frame.

Successive shortest paths

Canceling repairs a bad flow. The other classic method never builds one. Start from no flow and repeat one step: find the cheapest path from s to t in the residual graph and push as much along it as it holds (or as much as is still needed). Because refund arcs are negative, the cheapest path is found with Bellman–Ford, not Dijkstra, for now.

On the courier network, 5 parcels take three pushes:

push cheapest path parcels cost per parcel total so far
1 s→a→b→t 2 1+2+2=5 10
2 s→a→t 2 1+7=8 26
3 s→b→a→t 1 6−2+7=11 37

The first push is limited by b→t (capacity 2), the second by s→a (2 of its 4 left). The third is the interesting one: s→a is now full, so the only way out of s is s→b, and from b the path takes the refund arc b→a at −2. One of the two parcels that went a→b is taken back and sent a→t, and the new parcel takes its place on b→t. The result is the flow [4, 1, 1, 3, 0, 2, 0] at 37, the same one canceling found.

4/-1 1/6 2/2 2/-2 2/7 2/-7 1/3 2/-2 3/6 s δ=0 a δ=4 b δ=6 c δ=9 t δ=11
Before push 3 (4 parcels sent, cost 26): the residual graph, each arc room/cost, and under each vertex its cheapest cost δ from s (s 0, a 4, b 6, c 9, t 11). The cheapest path s → b → a → t (purple) costs 6 − 2 + 7 = 11 and uses the refund arc b → a.

Two things stand out. The costs per parcel, 5, 5, 8, 8, 11, never decrease. And every intermediate flow is itself the cheapest of its value. The block below checks both against brute force, which here stands in for the proof that follows:

per_parcel = [5, 5, 8, 8, 11]
assert per_parcel == sorted(per_parcel)
assert [cheapest_by_brute_force(k)[0] for k in (1, 2, 3, 4, 5)] == [5, 10, 18, 26, 37]
assert [sum(per_parcel[:k]) for k in (1, 2, 3, 4, 5)] == [5, 10, 18, 26, 37]
Predict: 4 parcels cost 26 after push 2. Could a cleverer routing of just those 4 cost less?

No. Brute force says the cheapest flow of value 4 costs 26. Successive shortest paths never passes through an expensive intermediate flow: after every push, the flow is the cheapest of its value. The next section proves it.

Why the cheapest path keeps the certificate

The claim is that after every push, the residual graph has no negative cycle, so by the theorem the current flow is the cheapest of its value. It holds at the start: with no flow there are no reverse arcs, and every edge cost is at least 0.

Suppose it holds before a push, and let δ(v) be the cheapest cost from s to v in Gf (∞ if v can't be reached). Cheapest costs satisfy the triangle inequality δ(v)≤δ(u)+a(u,v) on every residual arc, which says that the reduced cost

aδ(u,v)=a(u,v)+δ(u)−δ(v)≥0.

Along the cheapest path to t the inequality is tight, so each of its arcs has reduced cost exactly 0. Now push. Arcs off the path are unchanged. Arcs on the path may lose room or vanish. The only new arcs are reverses of path arcs, with reduced cost −0=0. So after the push every residual arc still has aδ≥0.

Finally, around any cycle the δ terms telescope: each δ(v) is added once, when the cycle leaves v, and subtracted once, when it enters. A cycle's cost equals its reduced cost, which is a sum of non-negative terms. That needs every δ on the cycle to be finite. An arc out of a reached vertex leads to a reached vertex, so a cycle is either wholly reached, where the argument applies, or wholly unreached. The push changes no arc between unreached vertices, so an unreached cycle was already there before the push and is non-negative by hypothesis. No cycle is negative. ◻

Invariant

After every push along a cheapest residual path, every residual arc has reduced cost a(u,v)+δ(u)−δ(v)≥0, so no residual cycle is negative and the current flow is the cheapest flow of its value.

The step not to skip is the path being cheapest. Push along any other path and one of its arcs has positive reduced cost. Its reverse then has negative reduced cost, and a negative cycle can appear. The same argument shows why the per-parcel costs never fall. Distances measured in reduced costs are at least 0, so the next cheapest path costs at least the last δ(t).

delta = {"s": 0, "a": 4, "b": 6, "c": 9, "t": 11}
arcs = [("a", "s", -1), ("s", "b", 6), ("a", "b", 2), ("b", "a", -2), ("a", "t", 7),
        ("t", "a", -7), ("b", "c", 3), ("t", "b", -2), ("c", "a", 6)]   # residual arcs before push 3
reduced = {(u, v): cost + delta[u] - delta[v] for u, v, cost in arcs}
assert all(rc >= 0 for rc in reduced.values())
assert reduced[("s", "b")] == reduced[("b", "a")] == reduced[("a", "t")] == 0     # the path
loop = [("a", "b"), ("b", "c"), ("c", "a")]
assert sum(COST[e] for e in loop) == sum(reduced[e] for e in loop) == 11          # telescoping

Potentials: Dijkstra with negative arcs

Bellman–Ford costs up to V−1 rounds over all 2E residual arcs per search. Dijkstra would cost one pass, but it is wrong on negative arcs. The proof above suggests the way round it. Keep a number π(v) for every vertex, called a potential, and search on reduced costs aπ(u,v)=a(u,v)+π(u)−π(v) instead of real costs. Every s–t path's reduced cost is its real cost plus π(s)−π(t), the same constant for all of them, so the cheapest path doesn't change. Around a cycle the π terms cancel completely, so a cycle's reduced cost is its real cost. If every reduced cost is at least 0, Dijkstra is exact.

Lemma (potentials). Suppose every residual arc has aπ≥0, and let δπ be the Dijkstra distances from s under those reduced costs. Then after the push along the path found, π′(v)=π(v)+min(δπ(v),δπ(t)) again gives every residual arc a reduced cost of at least 0. Proof: the same argument one level up. The reduced costs under π′ are the reduced costs under π re-reduced by min(δπ,δπ(t)). Capping a distance at a constant keeps the triangle inequality, so every old arc stays at 0 or more. Every vertex on the path has δπ≤δπ(t), so the path's arcs stay tight and their new reverses get 0. The cap changes every vertex farther than t, including those the search never reached (δπ=∞), whose potentials it keeps finite. ◻

With all edge costs at least 0, π=0 is a valid start: with no flow every residual arc is a forward arc, and its reduced cost is its cost. With some negative edge costs but no negative cycle, one Bellman–Ford run supplies the first π: take π(v) to be the cheapest cost to v from a virtual source joined to every vertex at cost 0, which satisfies the triangle inequality on every arc. Every later search is a Dijkstra.

Cost

We count arc scans, with integer capacities and costs, in the worst case. Each push moves at least one unit, so there are at most d pushes plus one failing search:

  • with Bellman–Ford, O(VE) per search, O(d·VE) in all;
  • with potentials and a binary-heap Dijkstra, O(ElogV) per search, O(d·ElogV).

Both are pseudo-polynomial: the number of pushes is bounded by d, a number in the input, not by the input's size in bits, so the bounds are polynomial in V, E and d but not in the number of bits it takes to write d down. Cycle canceling also terminates on integer data, but its count depends on the costs. Always canceling a cycle of least mean cost is polynomial in the size alone (Goldberg and Tarjan; cited, not proved here).

Assignment is a special case

Four workers, four jobs, and a cost for each worker–job pair:

w0 w1 w2 w3 j0 j1 j2 j3 4 1 2 6 4 7 8 7 5 9 8 5 3 2 6 7
The cost matrix. Highlighted: the cheapest assignment, w0→j2, w1→j0, w2→j3, w3→j1, total 2 + 4 + 5 + 2 = 13. It is the only one of the 24 that costs 13.

Build a network: s→ worker i (capacity 1, cost 0), worker i→ job j (capacity 1, cost Cij), job j→t (capacity 1, cost 0), and send d=4. Integer capacities give an integer cheapest flow, because every push moves a whole number of units, so each worker sends exactly one unit along exactly one pair. The cheapest flow is the cheapest assignment. It takes n pushes of a Dijkstra over n2 arcs: O(n3logn). The Hungarian method (Kuhn, 1955), a specialised version of the same idea, can be implemented in O(n3).

The greedy alternatives are cheaper to run and more expensive to follow. Giving each worker in turn their cheapest free job costs 16, and taking the cheapest free cells first costs 17. Neither ever has to break a tie on this matrix, so no tie rule rescues them; the block asserts that each choice is the only cheapest one:

from itertools import permutations

M = [[4, 1, 2, 6], [4, 7, 8, 7], [5, 9, 8, 5], [3, 2, 6, 7]]
costs = sorted((sum(M[i][p[i]] for i in range(4)), p) for p in permutations(range(4)))
assert costs[0] == (13, (2, 0, 3, 1)) and costs[1][0] > 13

def only_cheapest(options):
    """The cheapest option, after checking that no other option ties with it."""
    options = sorted(options)
    assert len(options) == 1 or options[0][0] < options[1][0], f"a tie: {options[:2]}"
    return options[0]

used, row_greedy = [], 0
for i in range(4):
    m, j = only_cheapest((M[i][j], j) for j in range(4) if j not in used)
    used.append(j)
    row_greedy += m
rows, cols, cell_greedy = set(), set(), 0
while len(rows) < 4:
    m, i, j = only_cheapest((M[i][j], i, j) for i in range(4) for j in range(4)
                            if i not in rows and j not in cols)
    rows.add(i)
    cols.add(j)
    cell_greedy += m
assert used == [1, 0, 3, 2] and (row_greedy, cell_greedy) == (16, 17)
assert M[0][2] + M[3][1] - M[0][1] - M[3][2] == 2 + 2 - 1 - 6 == -3

Row greedy gives worker 0 job 1 (cost 1) and leaves worker 3 to pay 6 for job 2. The residual graph of its answer has a negative cycle: swap the jobs of workers 0 and 3, and the total falls by 3, to the optimum 13.

Measure the claim

The bound says one Dijkstra pass per search against up to V−1 Bellman–Ford rounds. Here are arc scans on random networks (about 6V edges, capacities 1–5, costs 1–20, seeds 451+V), sending the maximum flow d from vertex 0 to vertex V−1:

V E d pushes Dijkstra scans Bellman–Ford scans per push ÷ 2E (Dijkstra, BF)
30 179 19 16 5,728 33,652 1.00, 5.9
60 347 10 9 6,246 42,334 1.00, 6.8
120 707 15 13 18,382 144,228 1.00, 7.8
rows = [(30, 179, 16, 5728, 33652), (60, 347, 9, 6246, 42334), (120, 707, 13, 18382, 144228)]
assert [round(dij / pushes / (2 * E), 2) for V, E, pushes, dij, bf in rows] == [1.0, 1.0, 1.0]
assert [round(bf / pushes / (2 * E), 1) for V, E, pushes, dij, bf in rows] == [5.9, 6.8, 7.8]

Dijkstra scans each arc about once per push, exactly as the proof says. Bellman–Ford needs 6 to 8 rounds, far below its worst case of V−1 = 29, 59 and 119, because on these networks the cheapest paths have few arcs and the loop stops early when a round changes nothing. That is a measurement on these instances, not a guarantee. The gap still grows with V. Every push stayed within its bound, and there were never more pushes than d.

A problem that looks different

Three warehouses hold 40, 25 and 35 pallets. Four stores need 30, 20, 30 and 20. Shipping one pallet from warehouse i to store j costs a known amount, and each route can carry any number. What is the cheapest shipping plan? Nothing in it mentions paths or cycles. Where are the source, the sink and the capacities? The lab's last problem is a different one.

Practise

In the lab you give residual arcs their refund prices and cancel negative cycles on a new network, watching the cost fall in every frame; predict, then trace, successive shortest paths, refund arc included; write the version with potentials and Dijkstra, with a check that reads every frame and asserts that no reduced cost is negative, and solve an assignment with it; count arc scans for both searches on your own networks; and build a roster for a problem that doesn't say what it is.

Recap

You can now: price residual arcs (a reverse arc costs −a(u,v), a refund); prove that a feasible flow of value d is the cheapest among flows of value d exactly when its residual graph has no negative cycle; run successive shortest paths with Bellman–Ford, or with potentials and Dijkstra; repair a flow by canceling cycles; and solve assignment as a special case.

Invariant: after every push along a cheapest residual path, every residual arc has reduced cost at least 0, so no cycle is negative and the flow is the cheapest of its value.

Complexity achieved: O(d·ElogV) with potentials, against O(d·VE) with Bellman–Ford, integer data, worst case; pseudo-polynomial, because the bound grows with the value d, not with its number of bits. Trying every integer flow is exponential in the number of edges.

Failure mode: reverse arcs priced at +a instead of −a. The code runs and returns a flow that is not the cheapest (here 51 would pass for optimal).

In real software: Google OR-Tools provides SimpleMinCostFlow and SimpleLinearSumAssignment; SciPy's scipy.optimize.linear_sum_assignment solves the assignment problem; NetworkX offers min_cost_flow and network_simplex.

Retrieval (Dynamic Programming II): cycle canceling needs a negative cycle in the residual graph. How could Floyd–Warshall find one, and what would it cost against Bellman–Ford?

Check yourself

  1. Why does pushing along a cheapest path keep the residual graph free of negative cycles? What goes wrong with a path that is not the cheapest?
  2. Give a→b a cost of −3 and leave the rest of the courier network alone. What changes in how you start, and what does the cheapest routing of 5 parcels cost now?
  3. On the 4 × 4 matrix, compare row greedy, cheapest-cell greedy and minimum-cost flow. Which residual cycle shows that row greedy's answer is not the cheapest?

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. Refund arcs, and canceling a negative cycle

    Every edge of a network now has a cost per unit as well as a capacity, and we want to send a fixed amount from s to t as cheaply as possible. This exercise starts from a flow that already sends all 6 units, but wastefully, and makes it cheaper one step at a time.

    A network is a list of edges (u, v, capacity, cost), and a flow is a list with one amount per edge, in the same order. A residual arc is an Arc(u, v, room, cost, edge, forward): it can carry room more units from u to v at cost each, and it belongs to edges[edge]. A forward arc sends more along its edge. A reverse arc takes flow back, and taking a unit back refunds its cost, so a reverse arc's cost is minus the edge's cost.

    First, fix residual_arcs(edges, flow). It must list every arc with room above 0: for each edge in order, its forward arc (if the edge has spare capacity), then its reverse arc (if the edge carries flow). The starter lists only forward arcs. Second, write cancel_cycle(edges, flow, cycle). Given a list of arcs that forms a cycle, push its bottleneck (the smallest room) around it by changing flow in place, and return the bottleneck and the cycle's cost per unit.

    Going once around a cycle keeps every vertex balanced and the value unchanged, so a cycle whose costs add up to less than 0 is a way to save money. find_negative_cycle (Bellman–Ford, as in Dynamic Programming II) is given. The program cancels cycles until none is left and draws one frame per cycle, with the cycle in purple and every residual arc as room/cost. The wasteful flow costs 75. Before you press Run: which arcs out of s exist under it, and what do they cost?

  2. Trace successive shortest paths, refund arcs included

    Now build the cheapest flow from nothing. Successive shortest paths starts with no flow and repeats one step: find the cheapest path from s to t in the residual graph and push as much along it as it holds (or as much as is still needed). Answer the three questions before you run anything.

    The network is the one from exercise 1, s→p 3/2, s→q 4/3, s→r 1/2, p→q 3/8, p→r 1/6, q→r 4/3, q→t 3/8, r→t 3/1 (capacity/cost), and we send 6 units.

    Write cheapest_paths(nodes, arcs, s). It returns (dist, parent): dist[v] is the cheapest cost of a path from s to v along the residual arcs (INF if there is none) and parent[v] is the last arc of such a path. Some arcs are refunds with negative costs, so use Bellman–Ford, not Dijkstra. residual_arcs from exercise 1 is given, and so is the loop in ssp_trace, which walks back from t along parent and pushes.

    The program draws one frame before each push: the residual graph with every arc as room/cost, the path in purple, and each vertex's cheapest cost d from s. A last frame shows the end. The check reads every frame and confirms that no residual cycle is negative in any of them, which is what makes each intermediate flow the cheapest of its value.

  3. Potentials and Dijkstra, then assignment as a special case

    Bellman–Ford handles the negative refund arcs, but it pays for that by scanning every arc up to V − 1 times per search. Dijkstra scans each arc once, but it is only correct when no arc is negative. Potentials reconcile the two. Keep a number π(v) for every vertex and price each residual arc at its reduced cost cost + π(u) − π(v). On any path from s to t the π terms telescope, so every path's reduced cost is its real cost plus π(s) − π(t), and the cheapest path stays the cheapest. The invariant to keep is that every reduced cost is at least 0, so Dijkstra is safe.

    Write min_cost_flow(nodes, edges, s, t, d, show=None). Start with no flow and π = 0, which is valid because every edge cost here is at least 0. Each round, run Dijkstra on reduced costs, push along the path it finds, and then add min(dist[v], dist[t]) to every π(v). It returns (cost, flow), or None if d units can't get through. When show is given, call show(edges, flow, pi, path) just before each push and show(edges, flow, pi, None) at the end. draw labels each vertex with π and lists every arc's reduced cost in large type. The check reads each frame and asserts that no reduced cost is negative in any of them. Leave π at 0 and the refund arcs will fail it. The checks read π, the flow and the answers, not how the search ran: a Bellman–Ford on reduced costs would pass them too. Exercise 4 counts what Dijkstra saves.

    Then write assignment(C). It gives each row of the square matrix C its own column so that the total is smallest, and returns (total, column of each row). Build a network in which every row and every column carries at most one unit, and call your min_cost_flow. Integer capacities give an integer flow, so each row gets exactly one column. On the demo's C, giving each row in turn its cheapest free column totals 22 (no row ever faces a tie, so no tie rule changes that). What does the best assignment cost?

  4. Measure: arc scans with and without potentials

    The proof says potentials let each search run as one Dijkstra pass instead of up to V − 1 Bellman–Ford rounds. Count it. The unit of work is an arc scan: one look at one residual arc, whether or not it has room.

    Both versions of successive shortest paths are given, on a compact residual structure where arc i and arc i ^ 1 are reverses. Each sends as much as possible from vertex 0 to vertex V − 1, as cheaply as possible, and stops when a search finds no path. First, make them count. Add 1 to stats["scans"] every time an arc is looked at, and 1 to stats["searches"] for every search, including the last one that fails. Count searches in both versions: they must agree. Second, write profile(sizes, seed=1300). For each V it builds random_network(V, seed + V) (about 6V edges, capacities 1–5, costs 1–20), runs both versions, and returns (V, E, units sent, cost, searches, Dijkstra scans, Bellman–Ford scans). A check also runs it on a seed of its own and compares your counts with its own count, so the counting has to happen in the code.

    The program draws scans per search divided by 2E for V = 30, 60 and 120. Dijkstra's bound is 1, and Bellman–Ford's is V − 1. Before you run: will the teal bars stay flat? Will the amber ones come anywhere near 29, 59 and 119?

  5. Mastery challenge: next week's clinic roster

    A clinic builds next week's roster from a survey. cost[i][j] is how much person i dislikes shift j (a small integer), or None if they can't work it. Person i works at most max_shifts[i] shifts, and shift j needs exactly need[j] different people. The roster is rebuilt every time someone swaps a shift, for 120 people and 60 shifts.

    Write staff_shifts(cost, max_shifts, need). It returns (total dislike, sorted list of (person, shift)) for a roster with the smallest total, or None if no roster fills every shift. The manager cancels shifts only on your word, so None must mean that none exists.

    The starter is correct but tries every roster, shift by shift. The check measures work: each dislike in cost arrives as a counted number, and every addition, subtraction, negation, multiplication or comparison involving a dislike, or a number computed from one, is one operation (int() keeps it counted). A check runs a week of 60 people and 30 shifts, once when it can be staffed and once when it can't, and allows 1.5 million operations for each. The starter runs out long before it finishes. (The count is feedback, not security: it can be dodged on purpose, and a deliberate dodge proves nothing.)

    Answer the four questions, then write it. The demo draws the demo week's survey with the chosen pairs shaded.