Unit 9: Shortest Roads
Three algorithms, three prices.
Unit 9 of 25 in Competitive programming for kids. Its 4 lessons are Keep Relaxing, Dijkstra, Every Pair at Once and Which One, and When — 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.
🛣️ Keep Relaxing
Roads have lengths now, and BFS is finished
Fewest roads is not cheapest journey. Two short hops can easily beat one long one, and BFS counts hops.
Every algorithm in this unit is built from one move, called relaxing an edge: if going to a and then along the road to b beats what you had written down for b, write down the better number.
Relax every road, over and over
Write 0 for the start and "unknown" for everything else, then relax every edge, n - 1 times over. That is Bellman–Ford.
The edges here are deliberately listed in an unhelpful order so you can watch the answers crawl outwards one round at a time.
Python
n = 5
edges = [(3, 4, 2), (2, 3, 9), (1, 3, 6), (2, 4, 7), (2, 1, 1), (0, 2, 3), (0, 1, 5)]
big = 99
dist = [big] * n
dist[0] = 0
for round in range(n - 1):
for a, b, w in edges:
if dist[a] + w < dist[b]:
dist[b] = dist[a] + w
print(dist)
It prints
[0, 5, 3, 99, 99] [0, 4, 3, 11, 10] [0, 4, 3, 10, 10] [0, 4, 3, 10, 10]
Why n - 1 rounds is always enough
A shortest route can visit each node at most once, so it uses at most n - 1 roads. After one round every one-road route is known, after two rounds every two-road route, and so on.
Cost: rounds times edges, so O(n * m). Slow — but it is the only one here that survives a negative road.
And an extra round is a lie detector
If anything still improves on round n, some loop makes the journey cheaper every time round it. That is a negative cycle, and "the shortest path" then has no answer at all.
The distances printed below are not wrong so much as meaningless — go round the loop again and they drop again. Contests ask for exactly this check, so run one more round and report it.
Python
n = 3
edges = [(0, 1, 1), (1, 2, -1), (2, 1, -1)]
big = 99
dist = [big] * n
dist[0] = 0
for round in range(n - 1):
for a, b, w in edges:
if dist[a] + w < dist[b]:
dist[b] = dist[a] + w
changed = False
for a, b, w in edges:
if dist[a] + w < dist[b]:
changed = True
print(dist, changed)
It prints
[0, -3, -2] True
Try it yourself
Why does Bellman–Ford run its rounds n - 1 times and not n?
- To save time
- A shortest route can never use more than `n - 1` roads
- Because arrays start at 0
- It is arbitrary
What does this print?
Python
n = 3
edges = [(0, 1, 4), (0, 2, 1), (2, 1, 1)]
big = 99
dist = [big] * n
dist[0] = 0
for round in range(n - 1):
for a, b, w in edges:
if dist[a] + w < dist[b]:
dist[b] = dist[a] + w
print(dist)
Answer them in the app
⛰️ Dijkstra
Relax in the right order and once is enough
Bellman–Ford relaxes everything again and again because it does not know which nodes are finished. Dijkstra always works on the nearest unfinished node — and once you are working on it, nothing can ever make it cheaper.
So every node is dealt with once, and the cost drops from n * m to about m log n.
The heap picks the nearest one for you
heapq keeps the smallest item at the front. Push (distance, node), always pop the smallest, and skip anything you have already beaten.
Watch the order the nodes come out in: 0, 2, 1, 3, 4 — nearest first, never in the order they were found.
Python
import heapq
n = 5
roads = [(0, 1, 5), (0, 2, 3), (2, 1, 1), (1, 3, 6), (2, 3, 9), (2, 4, 7), (3, 4, 2)]
adj = [[] for i in range(n)]
for a, b, w in roads:
adj[a].append((b, w))
big = 10 ** 9
dist = [big] * n
dist[0] = 0
heap = [(0, 0)]
order = []
while heap:
d, node = heapq.heappop(heap)
if d > dist[node]:
continue
order.append(node)
for nxt, w in adj[node]:
if d + w < dist[nxt]:
dist[nxt] = d + w
heapq.heappush(heap, (dist[nxt], nxt))
print(dist)
print(order)
It prints
[0, 4, 3, 10, 10] [0, 2, 1, 3, 4]
The skip line is not optional
A node can be pushed several times, once for each better route found. if d > dist[node]: continue throws away the stale copies.
Without it the algorithm still gets the right answer and gets slower and slower on big graphs, which is the worst kind of bug: invisible on your test, fatal on theirs.
A negative road breaks it, and here is the exact moment
Dijkstra closes node 1 at distance 2, because 2 is the smallest thing on the heap — and immediately sends 1 onward to node 3, arriving at 4.
Then the road from 2 to 1 turns out to cost -4, so node 1 was really only 1 away. Too late: it is closed, and node 3 keeps the stale 4. Bellman–Ford never closes anything, and gets 3.
Python
import heapq
roads = [(0, 1, 2), (1, 3, 2), (0, 2, 5), (2, 1, -4)]
n = 4
adj = [[] for i in range(n)]
for a, b, w in roads:
adj[a].append((b, w))
big = 99
dist = [big] * n
dist[0] = 0
heap = [(0, 0)]
done = [False] * n
while heap:
d, node = heapq.heappop(heap)
if done[node]:
continue
done[node] = True
for nxt, w in adj[node]:
if d + w < dist[nxt]:
dist[nxt] = d + w
heapq.heappush(heap, (dist[nxt], nxt))
slow = [big] * n
slow[0] = 0
for round in range(n - 1):
for a, b, w in roads:
if slow[a] + w < slow[b]:
slow[b] = slow[a] + w
print(dist)
print(slow)
It prints
[0, 1, 5, 4] [0, 1, 5, 3]
Try it yourself
Which algorithm would you write for 200000 nodes, 500000 roads and no negative lengths?
- Bellman–Ford
- Dijkstra with a heap
- Floyd–Warshall
- BFS
What does this print?
Python
import heapq
heap = []
heapq.heappush(heap, (5, "a"))
heapq.heappush(heap, (2, "b"))
heapq.heappush(heap, (7, "c"))
print(heapq.heappop(heap))
print(heapq.heappop(heap))
Answer them in the app
🔁 Every Pair at Once
Sometimes the question is about every pair
"How far is it from anywhere to anywhere?" Running Dijkstra from every node works. For small graphs there is something shorter to write and much easier to get right.
Floyd–Warshall: three nested loops over a table of distances, and that is the entire algorithm.
Let each node take a turn as a stopping point
The outer loop is not a node you are going to — it is a node you are allowed to stop at. After the round for k, the table holds the best journey using only stops from the ones tried so far.
The order of the loops matters completely: k on the outside, always.
Python
big = 99
n = 4
d = [
[0, 5, big, 10],
[big, 0, 3, big],
[big, big, 0, 1],
[big, big, big, 0],
]
for k in range(n):
for i in range(n):
for j in range(n):
if d[i][k] + d[k][j] < d[i][j]:
d[i][j] = d[i][k] + d[k][j]
for row in d:
print(row)
It prints
[0, 5, 8, 9] [99, 0, 3, 4] [99, 99, 0, 1] [99, 99, 99, 0]
Nine beats ten
There is a direct road from 0 to 3 costing 10. Going 0 to 1 to 2 to 3 costs 5 + 3 + 1 = 9, and the table found it.
Cost: n * n * n. At 500 nodes that is 125 million — just about affordable, and about the limit. Past that, run Dijkstra from each node instead.
Try it yourself
Which loop has to be the outermost one?
- The `i` loop, over starting nodes
- The `j` loop, over ending nodes
- The `k` loop, over allowed stopping points
- Any of them
What does this print?
Python
big = 99
n = 3
d = [
[0, 4, big],
[big, 0, 2],
[big, big, 0],
]
for k in range(n):
for i in range(n):
for j in range(n):
if d[i][k] + d[k][j] < d[i][j]:
d[i][j] = d[i][k] + d[k][j]
print(d[0][2])
Answer them in the app
🏆 Which One, and When
Four algorithms, one question each
Every edge costs the same → BFS, O(n + m).
One start, no negative roads → Dijkstra, about m log n.
One start, negative roads allowed → Bellman–Ford, n * m, and it detects a negative cycle.
Every pair, and n is small → Floyd–Warshall, n * n * n.
The numbers that make the choice for you
With 100000 nodes and 200000 roads, Dijkstra is about three and a half million steps and Bellman–Ford is twenty thousand million. There is nothing to think about.
With 300 nodes, Floyd–Warshall is 27 million and takes four lines. Also nothing to think about.
Python
import math
n = 100000
m = 200000
print("dijkstra", m * math.floor(math.log2(n)))
print("bellman", n * m)
print("floyd 300", 300 ** 3)
It prints
dijkstra 3200000 bellman 20000000000 floyd 300 27000000
The negative cycle question is a real one
A currency exchange where a loop of trades leaves you richer, a game where a loop scores points for ever — problems are written around exactly this, and the answer they want is "there is no cheapest journey".
One extra Bellman–Ford round finds it. Nothing else in the unit can.
Try it yourself
500 nodes, and you need the distance between every pair. What do you write?
- Floyd–Warshall
- Bellman–Ford from every node
- BFS from every node
- Two pointers
A road has length -3. Which of these still gives the right answer?
- Dijkstra
- BFS
- Bellman–Ford
- Dijkstra, if you add 3 to every road first
Answer them in the app