Unit 23: Flows
How much can get through at once.
Unit 23 of 25 in Competitive programming for kids. Its 4 lessons are Pipes and Undoing, Edmonds–Karp, Cuts and Matchings and The Cheapest Way Through — below is everything each one explains, and a question or two from it to try.
Every sample on this page was run through real Python before it shipped, and prints exactly what it says it prints.
This unit opens with a fortnight’s trial of everything — no card needed — or with a family plan, bought in the iPhone app. The first two units of every track are free for ever. Try it in the app.
🚰 Pipes and Undoing
Water from the source to the sink
Every pipe has a capacity. Water enters at the source and leaves at the sink, and at every other junction as much goes out as comes in.
How much can flow at once? Here 5 — because the two pipes leaving the source hold 3 and 2, and nothing can beat that.
Python
pipes = [(0, 1, 3), (0, 2, 2), (1, 2, 1), (1, 3, 2), (2, 3, 3)]
out_of_source = sum(c for a, b, c in pipes if a == 0)
into_sink = sum(c for a, b, c in pipes if b == 3)
print(out_of_source, into_sink)
It prints
5 5
Greedy paths get stuck
Take any route with room left, push as much as it holds, repeat. On this little graph — every pipe holding 1 — the route 0, 1, 2, 3 uses up the middle pipe and blocks both of the others.
Greedy stops at 1. The real answer is 2.
Python
pipes = [(0, 1, 1), (0, 2, 1), (1, 2, 1), (1, 3, 1), (2, 3, 1)]
print("route 0-1-2-3 carries", 1)
print("then 0-1-3 is blocked at 0-1, and 0-2-3 is blocked at 2-3")
print("greedy total", 1)
It prints
route 0-1-2-3 carries 1 then 0-1-3 is blocked at 0-1, and 0-2-3 is blocked at 2-3 greedy total 1
So allow a later route to undo an earlier one
When 1 unit flows from 1 to 2, record room for 1 unit going backwards, from 2 to 1. That backwards pipe is not real — using it means cancelling part of the earlier route.
Now the route 0 to 2, backwards to 1, then 1 to 3 works, and the total is 2. The residual graph is what makes the whole subject possible.
Python
n = 4
cap = [[0] * n for i in range(n)]
for a, b, c in [(0, 1, 1), (0, 2, 1), (1, 2, 1), (1, 3, 1), (2, 3, 1)]:
cap[a][b] = c
def push(route, amount):
for i in range(len(route) - 1):
a = route[i]
b = route[i + 1]
cap[a][b] -= amount
cap[b][a] += amount
push([0, 1, 2, 3], 1)
print(cap[2][1], cap[1][2])
push([0, 2, 1, 3], 1)
print(cap[2][1], cap[1][2])
print("total", 2)
It prints
1 0 0 1 total 2
What the numbers mean afterwards
After the second push the middle pipe is back where it started — one route sent water through it and the other sent water back. What actually happens is 0 to 1 to 3 and 0 to 2 to 3, which is obvious in hindsight and was not obvious to the greedy walk.
The algorithm never has to be clever. It only has to be allowed to change its mind.
Try it yourself
What does pushing flow along a backwards pipe really mean?
- Water flowing uphill
- Cancelling part of an earlier route, freeing it to go elsewhere
- A mistake
- Adding a new pipe
What does this print?
Python
n = 3
cap = [[0] * n for i in range(n)]
cap[0][1] = 4
cap[1][2] = 2
route = [0, 1, 2]
amount = min(cap[0][1], cap[1][2])
for i in range(len(route) - 1):
a = route[i]
b = route[i + 1]
cap[a][b] -= amount
cap[b][a] += amount
print(amount, cap[0][1], cap[1][0])
Answer them in the app
💧 Edmonds–Karp
Find a route, push, repeat
While a route from source to sink still has room, push the most it can hold and update both directions.
Finding the route with BFS — fewest pipes — makes it Edmonds–Karp, which is guaranteed to finish in n * m * m steps. Finding it with DFS can crawl.
Python
from collections import deque
n = 4
cap = [[0] * n for i in range(n)]
for a, b, c in [(0, 1, 3), (0, 2, 2), (1, 2, 1), (1, 3, 2), (2, 3, 3)]:
cap[a][b] += c
def find_route(source, sink):
parent = [-1] * n
parent[source] = source
queue = deque([source])
while queue:
node = queue.popleft()
for nxt in range(n):
if parent[nxt] == -1 and cap[node][nxt] > 0:
parent[nxt] = node
queue.append(nxt)
return parent
flow = 0
while True:
parent = find_route(0, 3)
if parent[3] == -1:
break
amount = 10 ** 9
node = 3
while node != 0:
amount = min(amount, cap[parent[node]][node])
node = parent[node]
node = 3
while node != 0:
cap[parent[node]][node] -= amount
cap[node][parent[node]] += amount
node = parent[node]
flow += amount
print("pushed", amount)
print("max flow", flow)
It prints
pushed 2 pushed 2 pushed 1 max flow 5
Reading the run
Three routes, carrying 2, 2 and 1 — five in total, which matches the 5 that could possibly leave the source.
The third push is only found because the earlier ones can be partly undone. Take the backwards updates out and the answer comes out too small.
A bigger one
Six junctions and eight pipes. The answer is 7, and no amount of staring will get you there faster than running it.
This is where flow stops being something to do in your head, which is the point at which people start trusting the algorithm.
Python
from collections import deque
n = 6
cap = [[0] * n for i in range(n)]
for a, b, c in [(0, 1, 5), (0, 3, 4), (1, 2, 6), (2, 5, 8), (3, 1, 3), (3, 4, 1), (4, 5, 2), (2, 4, 3)]:
cap[a][b] += c
def maxflow(source, sink):
flow = 0
while True:
parent = [-1] * n
parent[source] = source
queue = deque([source])
while queue:
node = queue.popleft()
for nxt in range(n):
if parent[nxt] == -1 and cap[node][nxt] > 0:
parent[nxt] = node
queue.append(nxt)
if parent[sink] == -1:
return flow
amount = 10 ** 9
node = sink
while node != source:
amount = min(amount, cap[parent[node]][node])
node = parent[node]
node = sink
while node != source:
cap[parent[node]][node] -= amount
cap[node][parent[node]] += amount
node = parent[node]
flow += amount
print(maxflow(0, 5))
It prints
7
The grid is for teaching, not for contests
An n by n grid of capacities is easy to read and impossible at 100000 nodes. Real code keeps an edge list where each edge stores the index of its own reverse, so the update is edge.cap -= f and reverse.cap += f.
Same algorithm, same two lines, different bookkeeping.
Try it yourself
Why search for the route with BFS rather than DFS?
- BFS is always faster
- Shortest routes give a guaranteed step count; DFS can take a huge number of tiny pushes
- DFS cannot find a route
- It makes no difference
What does this print?
Python
cap = [[0, 3, 0], [0, 0, 2], [0, 0, 0]]
amount = 10 ** 9
route = [0, 1, 2]
for i in range(len(route) - 1):
amount = min(amount, cap[route[i]][route[i + 1]])
print(amount)
Answer them in the app
✂️ Cuts and Matchings
The narrowest place in the network
Split the junctions into the source's side and the sink's side. Add up the capacities of the pipes crossing from one side to the other: that is a cut.
No flow can beat any cut. And the astonishing part is that the best flow always equals the smallest cut.
Finding it: whatever the last search could still reach
When the flow finishes, run the search once more on what is left. The junctions it can still reach are the source's side; everything else is the sink's side.
The pipes crossing that line are exactly full, and their total is the flow.
Python
from collections import deque
n = 4
pipes = [(0, 1, 3), (0, 2, 2), (1, 2, 1), (1, 3, 2), (2, 3, 3)]
cap = [[0] * n for i in range(n)]
for a, b, c in pipes:
cap[a][b] += c
flow = 0
while True:
parent = [-1] * n
parent[0] = 0
queue = deque([0])
while queue:
node = queue.popleft()
for nxt in range(n):
if parent[nxt] == -1 and cap[node][nxt] > 0:
parent[nxt] = node
queue.append(nxt)
if parent[3] == -1:
break
amount = 10 ** 9
node = 3
while node != 0:
amount = min(amount, cap[parent[node]][node])
node = parent[node]
node = 3
while node != 0:
cap[parent[node]][node] -= amount
cap[node][parent[node]] += amount
node = parent[node]
flow += amount
side = [i for i in range(n) if parent[i] != -1]
crossing = [(a, b, c) for a, b, c in pipes if a in side and b not in side]
print(flow)
print(side)
print(crossing, sum(c for a, b, c in crossing))
It prints
5 [0] [(0, 1, 3), (0, 2, 2)] 5
Matching people to things is a flow
Three children, three books, and a list of who would read what. Give each child one book, each book to one child, as many as possible.
Make a source pointing at every child with capacity 1, every book pointing at a sink with capacity 1, and each "would read" a pipe of capacity 1. The greatest flow is the biggest matching.
Python
from collections import deque
n = 8
source = 6
sink = 7
likes = [(0, 3), (0, 4), (1, 3), (2, 4), (2, 5)]
cap = [[0] * n for i in range(n)]
for child in range(3):
cap[source][child] = 1
for book in range(3, 6):
cap[book][sink] = 1
for a, b in likes:
cap[a][b] = 1
flow = 0
while True:
parent = [-1] * n
parent[source] = source
queue = deque([source])
while queue:
node = queue.popleft()
for nxt in range(n):
if parent[nxt] == -1 and cap[node][nxt] > 0:
parent[nxt] = node
queue.append(nxt)
if parent[sink] == -1:
break
node = sink
while node != source:
cap[parent[node]][node] -= 1
cap[node][parent[node]] += 1
node = parent[node]
flow += 1
print(flow)
for a, b in likes:
if cap[b][a] == 1:
print(a, "reads", b)
It prints
3 0 reads 4 1 reads 3 2 reads 5
Why capacity 1 does the work
A capacity of 1 from the source means a child can be given at most one book; a capacity of 1 into the sink means a book goes to at most one child.
That is the whole modelling trick, and it is the skill flow problems are really testing: not the algorithm, which you copy, but seeing that a problem is a flow.
Try it yourself
A network has a cut of total capacity 7. What do you know about the flow?
- It is exactly 7
- It is at most 7
- It is at least 7
- Nothing
What does this print?
Python
pipes = [(0, 1, 3), (0, 2, 2), (1, 3, 2), (2, 3, 3)]
side = [0, 1]
crossing = [(a, b, c) for a, b, c in pipes if a in side and b not in side]
print(crossing, sum(c for a, b, c in crossing))
Answer them in the app
🏆 The Cheapest Way Through
Now every pipe charges by the litre
Each pipe has a capacity and a price per unit. Send a required amount from source to sink as cheaply as possible.
This is how delivery lorries, job assignments and network routing are actually modelled — and it is max flow with one change.
Always augment along the cheapest route
Instead of the shortest route in pipes, take the cheapest route in money — a shortest-path search where the length of a pipe is its price.
And the backwards pipes carry the negative of the price, because undoing a unit gives the money back. Negative lengths mean Dijkstra is out and unit 9's Bellman–Ford is in.
One unit, then two, then three
Five pipes. Sending one unit costs 2, two units cost 5, and all three cost 9 — the price per unit goes up, because the cheap routes fill first.
That is always true, and it is why augmenting cheapest-first is safe: no later route can make an earlier choice look wrong.
Python
from collections import deque
def min_cost_flow(n, pipes, source, sink, need):
graph = [[] for i in range(n)]
def add(a, b, capacity, price):
graph[a].append([b, capacity, price, len(graph[b])])
graph[b].append([a, 0, -price, len(graph[a]) - 1])
for a, b, capacity, price in pipes:
add(a, b, capacity, price)
big = 10 ** 9
sent = 0
spent = 0
while sent < need:
dist = [big] * n
dist[source] = 0
inside = [False] * n
from_node = [-1] * n
from_edge = [-1] * n
queue = deque([source])
inside[source] = True
while queue:
node = queue.popleft()
inside[node] = False
for i in range(len(graph[node])):
nxt, capacity, price, back = graph[node][i]
if capacity > 0 and dist[node] + price < dist[nxt]:
dist[nxt] = dist[node] + price
from_node[nxt] = node
from_edge[nxt] = i
if not inside[nxt]:
queue.append(nxt)
inside[nxt] = True
if dist[sink] == big:
break
amount = need - sent
node = sink
while node != source:
amount = min(amount, graph[from_node[node]][from_edge[node]][1])
node = from_node[node]
node = sink
while node != source:
edge = graph[from_node[node]][from_edge[node]]
edge[1] -= amount
graph[node][edge[3]][1] += amount
node = from_node[node]
sent += amount
spent += amount * dist[sink]
return sent, spent
pipes = [(0, 1, 2, 1), (0, 2, 2, 3), (1, 3, 1, 1), (1, 2, 1, 1), (2, 3, 2, 1)]
print(min_cost_flow(4, pipes, 0, 3, 1))
print(min_cost_flow(4, pipes, 0, 3, 2))
print(min_cost_flow(4, pipes, 0, 3, 3))
It prints
(1, 2) (2, 5) (3, 9)
Reading the answer
The first unit goes 0 to 1 to 3 and costs 2. The second goes 0 to 1 to 2 to 3 and costs 3, bringing the total to 5. The third has to use the dear pipe 0 to 2 and costs 4, so 9.
Ask for more than the network can carry and it stops early, returning what it managed — which is what the sent value is for.
Try it yourself
Why can Dijkstra not be used to find the cheapest augmenting route?
- It is too slow
- The backwards pipes have negative prices, which Dijkstra cannot handle
- The graph is too big
- It can be
What does this print?
Python
graph = [[] for i in range(2)]
def add(a, b, capacity, price):
graph[a].append([b, capacity, price, len(graph[b])])
graph[b].append([a, 0, -price, len(graph[a]) - 1])
add(0, 1, 5, 7)
print(graph[0][0][1], graph[0][0][2])
print(graph[1][0][1], graph[1][0][2])
Answer them in the app