Min-Cost Flow & Assignment
In short: Max flow asks how much can move. Min-cost flow asks how cheaply a given amount can move, when every pipe also has a price per unit. The algorithm is the familiar one, with the search for an augmenting path replaced by a search for the cheapest one: push units along the cheapest route with capacity left, update the residual graph, repeat. Because every reverse edge refunds its forward cost (its weight is the negative of the original), the residual graph always has negative edges — so the search uses Bellman-Ford, not Dijkstra, even though every pipe you started with had a non-negative cost. The assignment problem — pairing rows to columns for the least total cost — turns out to be the exact same algorithm on a network where every pipe has capacity one.

From matching to pricing
Section 17's assign_drivers found the biggest matching, with no notion of
one driver-job pairing being better than another. Give every pipe in that
same network a cost — what it's worth to Meridian to have ana driving the
truck instead of the van — and "the best assignment" becomes a min-cost
flow question with every capacity equal to 1. The four-pipe graph above is
exactly that: r1-c1 costing 4 instead of r1-c2 costing 1 is the
difference between a good pairing and a bad one, and checking both totals
(4 vs. 3) is the entire Hungarian algorithm's idea in miniature. A real
Hungarian solver avoids trying every permutation; this lesson's exercise
uses permutations directly, since the matrices Meridian deals with here are
small enough that it doesn't matter.
Why the residual graph needs Bellman-Ford
Pushing 4 units along a pipe that costs 1 per unit opens a reverse edge that costs −1 per unit — undoing a unit should refund what it cost, the same way undoing capacity should return it. That −1 is not a special case; it's true of every reverse edge, every time, even on a network where every original cost is positive:
Dijkstra assumes every edge weight is non-negative, which the residual graph breaks on the very first push. Bellman-Ford tolerates negative weights as long as there's no negative cycle — and a min-cost flow network never develops one, because a cycle that paid you to go around it would mean the flow you already had wasn't actually cheapest, a contradiction.
Watch it on the depot network
Costs: depot-dock and dock-axle are cheap (1 each), depot-mill is dear (3), axle-harbour is 2, and the berth detour is the dearest hop (4):
Edges badged with (capacity, cost). Each round finds the cheapest residual path and pushes its full bottleneck.
Send 5 units from depot to harbour for the least total cost. Costs: depot-dock 1, dock-axle 1, axle-harbour 2, depot-mill 3, mill-axle 1, dock-berth 4, berth-harbour 1. 6 nodes, 7 edges, directed. At depot.
The second path reuses the exact backward edge from lesson 1 — but this time it's not just freeing capacity, it's worth doing even though it looks like a detour, because the refund makes the full route cheaper than any alternative that avoids axle-dock entirely.
In code
Quick practice
# two routes: s-a-t at 1+1 per unit, s-b-t at 1+3 per unit
cheap_route = 1 + 1
dear_route = 1 + 3
# send=4 but each route only has capacity 3 -- 3 go cheap, 1 is forced dear
total_cost = 3 * cheap_route + 1 * dear_route
print(total_cost)push_with_cost updates capacity correctly but leaves cost untouched. Fix it so it also sets cost[b][a] = -cost[a][b] on every push.
Why min cost, not max flow, is the real question
A pure max-flow run doesn't care which route it uses, only that it finds a path with capacity. Min-cost flow adds a tiebreaker — among every way to reach the same total, prefer the cheapest — and that tiebreaker is why real logistics software runs this and not plain Edmonds-Karp. Meridian's dispatcher in the capstone leans on exactly this: not just "can 5 loads move", but "what's the least it costs to move them."
| Finds | Needs | |
|---|---|---|
| Max flow | the most that can move | BFS (Edmonds-Karp) or level graphs (Dinic) |
| Min-cost flow | the cheapest way to move a given amount | Bellman-Ford, because residual costs go negative |
| Assignment | the cheapest 1-to-1 pairing | min-cost flow with every capacity 1 |
❓ Cross-question: "What if I just want the cheapest way to max out the flow, not a fixed amount?" Set
sendto the network's actual max flow (lesson 1'spush_volume), or just let the loop run until no augmenting path exists at all — both give the min-cost maximum flow.
🎯 Your turn
Write cheapest_dispatch(edges, source, sink, send). edges is a list of
[from, to, capacity, cost]. Push each unit along the cheapest residual
path (Bellman-Ford) until send units have moved or no path remains:
cheapest_dispatch([["s", "a", 3, 1], ["a", "t", 3, 1]], "s", "t", 2)→{"flow": 2, "cost": 4}
Hint: track capacity and cost together per directed pair; a reverse
edge starts at capacity 0 and cost -w.
Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of Meridian's dispatcher is built. ✅
