Edges now have weights. Two different questions come with them: the cheapest way to connect everything (a spanning tree) and the cheapest way to reach each vertex from one source (shortest paths). Prim answers the first; Bellman-Ford and Dijkstra answer the second, and the activity shows why the answers are different trees.
Finish the minimum spanning tree: grow one tree by the cheapest edge leaving it
The lightest edge crossing any cut is in the MST. Each edge Kruskal takes is the lightest one crossing the cut around its piece; Prim uses the same fact with one piece growing.
KRUSKAL ON THE WEEK 7 GRAPH. TEAL: TAKEN. MAGENTA DASHED: B–D, THE FIRST EDGE SKIPPED.
A utility wants to run fiber between seven Oregon towns. Trenching costs by the mile, and a town only needs to be connected to the network, not to every other town. Which roads do you trench along?
APPROXIMATE ROAD MILES. PDX PORTLAND, HRV HOOD RIVER, SLM SALEM, CRV CORVALLIS, NWP NEWPORT, EUG EUGENE, BND BEND. TEAL: PRIM FROM SALEM.
The tree's vertex set S and everything else form a cut. Prim takes the lightest edge crossing it. That is the cut property from the greedy lecture, word for word.
“Give me the lightest edge leaving the tree” is a delete-min on a changing collection: a priority queue, built on the binary heap from Week 6.
TEAL VERTICES: IN THE TREE. AMBER: THE LIGHTEST EDGE LEAVING IT.
1 import heapq 2 3 def prim(adj: dict[str, list[tuple[str, int]]], s: str) -> list[tuple[str, str, int]]: 4 in_tree = {s} 5 tree: list[tuple[str, str, int]] = [] 6 pq = [(w, s, v) for v, w in adj[s]] # (weight, from, to) 7 heapq.heapify(pq) 8 while pq and len(in_tree) < len(adj): 9 w, u, v = heapq.heappop(pq) # lightest candidate 10 if v in in_tree: 11 continue # no longer crosses the cut 12 in_tree.add(v) 13 tree.append((u, v, w)) 14 for x, wx in adj[v]: 15 if x not in in_tree: 16 heapq.heappush(pq, (wx, v, x)) 17 return tree
When fewest edges is no longer lightest; negative edges and negative cycles
An electric car leaves Portland for Bend. Each segment costs battery, except the descent from Government Camp to Warm Springs, which puts charge back: a negative edge. Which route arrives with the most battery?
ILLUSTRATIVE NET kWh. PDX PORTLAND, GCP GOVERNMENT CAMP, HRV HOOD RIVER, WSP WARM SPRINGS, BND BEND. TEAL: THE LIGHTEST ROUTE.
THE RUNNING EXAMPLE. ONE EDGE HAS A NEGATIVE WEIGHT.
δ(s, v) is the smallest weight of any path from s to v. If v cannot be reached from s, δ(s, v) = ∞.
TEAL: THE SHORTEST-PATH TREE FROM S, WITH δ(S, v) AT EACH VERTEX.
Relax every edge |V| − 1 times
1 def relax(u: str, v: str, w: int, d: dict[str, float], parent: dict[str, str]) -> None: 2 if d[u] + w < d[v]: 3 d[v] = d[u] + w 4 parent[v] = u
Every finite d[v] is the weight of a real path from s to v, so d[v] ≥ δ(s, v) at all times: estimates can be too high, never too low. Once an estimate equals δ, it stays there.
If no negative cycle is reachable from s, then after |V| − 1 rounds d[v] = δ(s, v) for every vertex. Negative edge weights are fine.
|V| − 1 rounds, each touching all |E| edges: O(V · E).
Distance-vector routing (RIP, the early internet's routing protocol) is Bellman-Ford run by every router at once; currency-arbitrage finders turn exchange rates into weights and look for a negative cycle.
Edge order every round: C→D, A→C, B→C, B→D, B→A, S→A, S→B.
| after round | S | A | B | C | D |
|---|---|---|---|---|---|
| start | 0 | ∞ | ∞ | ∞ | ∞ |
| 1 | 0 | 5 | 2 | ∞ | ∞ |
| 2 | 0 | 3 | 2 | 8 | 8 |
AFTER ROUND 2. MAGENTA: ESTIMATES THAT CHANGED THIS ROUND.
| after round | S | A | B | C | D |
|---|---|---|---|---|---|
| start | 0 | ∞ | ∞ | ∞ | ∞ |
| 1 | 0 | 5 | 2 | ∞ | ∞ |
| 2 | 0 | 3 | 2 | 8 | 8 |
| 3 | 0 | 3 | 2 | 6 | 4 |
| 4 | 0 | 3 | 2 | 6 | 2 |
1 def bellman_ford(vertices: list[str], edges: list[tuple[str, str, int]], s: str) -> tuple[dict[str, float], dict[str, str | None]]: 2 d: dict[str, float] = {v: float('inf') for v in vertices} 3 parent: dict[str, str | None] = {v: None for v in vertices} 4 d[s] = 0 5 for _ in range(len(vertices) - 1): 6 for u, v, w in edges: 7 if d[u] + w < d[v]: # relax 8 d[v] = d[u] + w 9 parent[v] = u 10 for u, v, w in edges: # check pass 11 if d[u] + w < d[v]: 12 raise ValueError('negative cycle') 13 return d, parent
No negative edges? Relax in greedy order with a heap: O(E log V)
Same seven towns, same road miles, a different question: a delivery van based in Salem wants the shortest drive to each town. Road miles are never negative, and that is exactly the case Bellman-Ford is overkill for.
ROAD MILES FROM SALEM. TEAL: DIJKSTRA'S SHORTEST-PATH TREE. PRIM'S TREE USED CRV–EUG AND EUG–BND INSTEAD.
With no negative edges, any path leaving the settled set passes an unsettled vertex with d ≥ d[u] and only gets longer, so the smallest unsettled d is already δ(s, u). Prim's cut argument with d[u] + w in place of w.
Each edge relaxed once per settled endpoint, each improvement one heap push: O(E log V). Same heap as Prim; the key is d[u] + w(u, v), not w(u, v).
Turn-by-turn directions (A* is Dijkstra with a heuristic), link-state routing inside an ISP (OSPF runs Dijkstra on every router), and any weight that is a distance, a time, or a price.
1 Adj = dict[str, list[tuple[str, int]]] 2 Dist, Parent = dict[str, float], dict[str, str | None] 3 4 def dijkstra(adj: Adj, s: str) -> tuple[Dist, Parent]: 5 d: Dist = {v: float('inf') for v in adj} 6 parent: Parent = {v: None for v in adj} 7 d[s] = 0 8 pq: list[tuple[float, str]] = [(0, s)] 9 settled: set[str] = set() 10 while pq: 11 du, u = heapq.heappop(pq) 12 if u in settled: # stale entry 13 continue 14 settled.add(u) 15 for v, w in adj[u]: 16 if du + w < d[v]: # relax 17 d[v] = du + w 18 parent[v] = u 19 heapq.heappush(pq, (d[v], v)) 20 return d, parent
Prim and Dijkstra on one graph, Dijkstra on another, and one question about a negative edge
Side 1: a Willamette Valley fiber co-op (Salem to Eugene; weights are trenching cost). Run Prim from Salem for the cheapest network, then Dijkstra from Salem for the fastest routes: which towns does the cheapest network reach the long way, and why? Side 2: an electric shuttle from Timberline Lodge down to Troutdale. Run Dijkstra from the lodge for the route that uses the least battery; then the engineers measure one descent as regenerative, and you decide whether Dijkstra is still the right tool.
THE MST FROM S: S-B, B-A, A-C, C-E, E-D, WEIGHT 30.
| popped | d | S | A | B | C | D | E |
|---|---|---|---|---|---|---|---|
| S | 0 | 0 | 6 | 2 | ∞ | ∞ | ∞ |
| B | 2 | 0 | 6 | 2 | 9 | 13 | ∞ |
| A | 6 | 0 | 6 | 2 | 9 | 13 | ∞ |
| C | 9 | 0 | 6 | 2 | 9 | 13 | 19 |
| D | 13 | 0 | 6 | 2 | 9 | 13 | 19 |
| E | 19 | 0 | 6 | 2 | 9 | 13 | 19 |
THE SHORTEST-PATH TREE FROM S, WITH δ(S, v). COMPARE WITH THE MST.
| popped | d | S | P | Q | R | T |
|---|---|---|---|---|---|---|
| S | 0 | 0 | 7 | 3 | ∞ | ∞ |
| Q | 3 | 0 | 4 | 3 | 12 | 9 |
| P | 4 | 0 | 4 | 3 | 6 | 9 |
| R | 6 | 0 | 4 | 3 | 6 | 9 |
| T | 9 | 0 | 4 | 3 | 6 | 9 |
THE SHORTEST-PATH TREE FROM S WITH ALL LEGS NON-NEGATIVE. WITH R → T = −4 THE ROUTE TO T CHANGES.
What to keep, and what comes next