Outline

  1. Prim's algorithm
    Finish the minimum spanning tree: grow one tree by the cheapest edge leaving it
  2. Weighted shortest paths
    When fewest edges is no longer lightest; negative edges and negative cycles
  3. Bellman-Ford
    Relax every edge |V| − 1 times
  4. Dijkstra
    No negative edges? Relax in greedy order with a heap: O(E log V)
  5. Activity: Two Trees
    Prim and shortest paths on one graph, Bellman-Ford on another
  6. Wrap-up
    What to keep, and what comes next
CS 351

How to apply today's lecture

THE QUESTION

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.

CS 351
// SECTION

Prim's algorithm

Finish the minimum spanning tree: grow one tree by the cheapest edge leaving it

O(n²)O(n)O(log n)

Kruskal, and the union-find behind it

  • Kruskal: sort the edges by weight and walk them lightest first. Take an edge if its endpoints are in different pieces, skip it if it would close a cycle; stop at |V| − 1 edges.
  • Here: 1, 2, 3, 4 taken; B–D (5) skipped (B and D are already joined through A); then 6 and 7. Weight 23.
  • Union-find answers “same piece?”: find follows parent pointers to a root, union links two roots. With path compression and union by rank each call is nearly O(1), so the sort dominates: O(E log E).
WHY IT IS CORRECT

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.

425836971011112ABCDEFG

KRUSKAL ON THE WEEK 7 GRAPH. TEAL: TAKEN. MAGENTA DASHED: B–D, THE FIRST EDGE SKIPPED.

CS 351

Connect every town at the lowest total cost

THE USE CASE

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?

  • That is a minimum spanning tree: the cheapest set of edges that leaves everything connected.
  • Prim's answer: start anywhere (Salem), grab the cheapest road leaving your network, repeat. Six roads, 372 miles. The 160-mile Portland–Bend run is never chosen.
  • The same algorithm lays out power lines and water mains, and, run backwards, clusters data points (single-linkage clustering).
476236644653100128132140160PDXHRVSLMCRVNWPEUGBND

APPROXIMATE ROAD MILES. PDX PORTLAND, HRV HOOD RIVER, SLM SALEM, CRV CORVALLIS, NWP NEWPORT, EUG EUGENE, BND BEND. TEAL: PRIM FROM SALEM.

CS 351

The algorithm

  1. Pick any start vertex s. The tree is {s}.
  2. Among all edges with one endpoint in the tree and one outside, take the lightest. Add it and its outside endpoint to the tree.
  3. Repeat until every vertex is in the tree.
WHY EACH ADDED EDGE IS SAFE

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.

THE DATA STRUCTURE QUESTION

“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.

CS 351

Walkthrough 1 of 2: start at A

  • Tree {A}. Crossing edges: A–B (4), A–D (2). Take A–D (2).
  • Tree {A, D}. Crossing: A–B (4), B–D (5), D–F (7), D–E (9). Take A–B (4).
  • Tree {A, B, D}. Crossing: B–E (3), D–F (7), B–C (8), D–E (9). Take B–E (3).
  • B–D (5) silently stopped being a crossing edge when B joined: both endpoints are inside now.
  • Next: the crossing edges now include E–G (1).
425836971011112ABCDEFG

TEAL VERTICES: IN THE TREE. AMBER: THE LIGHTEST EDGE LEAVING IT.

CS 351

Walkthrough 2 of 2: the same tree, a different order

  • Take E–G (1), then C–E (6), then D–F (7). Six edges, seven vertices, done.
  • Prim's order: 2, 4, 3, 1, 6, 7. Kruskal's order on the same graph: 1, 2, 3, 4, 6, 7.
  • Same six edges, total weight 23. With distinct weights there is only one MST, so both must find it.
  • Prim's tree is connected at every step. Kruskal's forest is not.
425836971011112ABCDEFG
CS 351

Try it: Prim, one heap pop at a time

CS 351

Prim in Python, and its cost

 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
  • The heap holds candidate edges. Stale ones (both endpoints inside) are discarded when popped.
  • Each edge is pushed at most twice and popped at most twice.
  • Heap size is O(E), so each push or pop is O(log E) = O(log V).
  • Total: O(E log V), the same as Kruskal.
  • Kruskal or Prim? A data structure choice, not a correctness one: edge list and union-find, or adjacency lists and a heap.
CS 351
// SECTION

Weighted shortest paths

When fewest edges is no longer lightest; negative edges and negative cycles

O(n²)O(n)O(log n)

Portland to Bend on one charge

THE USE CASE

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?

  • Every map app solves this: a single-source shortest path, weighted in minutes, miles, dollars, or kWh.
  • Fewest segments is wrong (PDX ⁠→⁠ HRV ⁠→⁠ BND, 50 kWh). Lightest: PDX ⁠→⁠ GCP ⁠→⁠ WSP ⁠→⁠ BND, 18 − 2 + 14 = 30 kWh.
  • A negative cycle would be a loop that gains charge forever. Reality forbids it, so a model with one is wrong, and the algorithm must say so.
18129−2143824PDXGCPHRVWSPBND0 kWh18 kWh12 kWh16 kWh30 kWh

ILLUSTRATIVE NET kWh. PDX PORTLAND, GCP GOVERNMENT CAMP, HRV HOOD RIVER, WSP WARM SPRINGS, BND BEND. TEAL: THE LIGHTEST ROUTE.

CS 351

Weighted, directed graphs

  • Each edge (u, v) now has a direction and a weight w(u, v): a distance, a time, a cost.
  • The weight of a path is the sum of its edge weights.
  • Two ways from S to A here: the direct edge costs 5. Going S ⁠→⁠ B ⁠→⁠ A costs 2 + 1 = 3.
  • Fewest edges is not the same as lightest. Monday's BFS counts hops; with weights, that gives the wrong answer.
−4386152SABCD

THE RUNNING EXAMPLE. ONE EDGE HAS A NEGATIVE WEIGHT.

CS 351

The problem, and what we return

DEFINITION

δ(s, v) is the smallest weight of any path from s to v. If v cannot be reached from s, δ(s, v) = ∞.

  • Input: a weighted directed graph and a source vertex s. Output: δ(s, v) for every v.
  • As in BFS, store parent[v], the vertex before v on a shortest path; the parents form a shortest-path tree.
  • A negative edge is fine. A negative cycle (weights summing below zero) is not: every lap makes the path cheaper, so no shortest path exists. A correct algorithm must detect it.
−4386152SABCDd = 0d = 3d = 2d = 6d = 2

TEAL: THE SHORTEST-PATH TREE FROM S, WITH δ(S, v) AT EACH VERTEX.

CS 351
// SECTION

Bellman-Ford

Relax every edge |V| − 1 times

O(n²)O(n)O(log n)

Relaxation: one operation, repeated

  • Keep an estimate d[v] for every vertex: the weight of the best path to v found so far. Start with d[s] = 0, everything else ∞.
  • Relaxing an edge (u, v) asks one question: is going through u better than what I have for v?
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
SAFETY

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.

CS 351

Bellman-Ford: the algorithm

  1. Set d[s] = 0 and every other estimate to ∞.
  2. Repeat |V| − 1 times: relax every edge in the graph, in any fixed order.
  3. Make one more pass. If any edge can still be relaxed, report a negative cycle.
WHAT IT GUARANTEES

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.

COST

|V| − 1 rounds, each touching all |E| edges: O(V · E).

IN THE WILD

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.

CS 351

Walkthrough 1 of 2: rounds 1 and 2

Edge order every round: C⁠→⁠D, A⁠→⁠C, B⁠→⁠C, B⁠→⁠D, B⁠→⁠A, S⁠→⁠A, S⁠→⁠B.

  • Round 1. The first five edges have a tail at ∞: nothing to extend. S ⁠→⁠ A: d[A] = 5. S ⁠→⁠ B: d[B] = 2.
  • Round 2. A ⁠→⁠ C: d[C] = 5 + 3 = 8. B ⁠→⁠ D: d[D] = 2 + 6 = 8. B ⁠→⁠ A: 2 + 1 = 3 beats 5, so d[A] = 3.
  • d[C] = 8 was computed from the old d[A] = 5. It is now stale, and the next round will fix it.
after roundSABCD
start0∞∞∞∞
1052∞∞
203288
−4386152SABCDd = 0d = 3d = 2d = 8d = 8

AFTER ROUND 2. MAGENTA: ESTIMATES THAT CHANGED THIS ROUND.

CS 351

Walkthrough 2 of 2: rounds 3, 4, and the check

  • Round 3. C ⁠→⁠ D: d[D] = 8 − 4 = 4. A ⁠→⁠ C: 3 + 3 = 6 beats 8, so d[C] = 6. Now d[D] is stale.
  • Round 4 (round |V| − 1). C ⁠→⁠ D: 6 − 4 = 2 beats 4, so d[D] = 2. Every estimate now equals δ.
  • Check pass. No edge can be relaxed: no negative cycle.
  • The shortest path to D, S ⁠→⁠ B ⁠→⁠ A ⁠→⁠ C ⁠→⁠ D, has four edges and took four rounds. After round k, every vertex whose shortest path has at most k edges is correct; a simple path has at most |V| − 1 edges, which is why |V| − 1 rounds are enough.
after roundSABCD
start0∞∞∞∞
1052∞∞
203288
303264
403262
−4386152SABCDd = 0d = 3d = 2d = 6d = 2
CS 351

Try it: Bellman-Ford, one relaxation at a time

CS 351

Bellman-Ford in Python

 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
  • Two nested loops: O(V · E).
  • Early exit: if a whole round changes nothing, the answers are final; stop.
  • On a dense graph this is O(V³). With no negative weights, Dijkstra (next section) brings it to O(E log V) with the same heap Prim used.
CS 351
// SECTION

Dijkstra

No negative edges? Relax in greedy order with a heap: O(E log V)

O(n²)O(n)O(log n)

Salem to everywhere, by road

THE USE CASE

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.

  • Relax in a smarter order: settle the closest unsettled town next; a heap keyed by d[v] hands it over.
  • Pop order from Salem: Corvallis 36, Portland 47, Eugene 64, Newport 89, Hood River 109, Bend 132. Each town pops once, final.
  • Prim's fiber tree reached Bend through Corvallis and Eugene (210 miles of driving); the shortest-path tree takes Salem–Bend directly, 132. Same graph, two trees.
476236644653100128132140160PDXHRVSLMCRVNWPEUGBND36476489109132

ROAD MILES FROM SALEM. TEAL: DIJKSTRA'S SHORTEST-PATH TREE. PRIM'S TREE USED CRV–EUG AND EUG–BND INSTEAD.

CS 351

Dijkstra: the algorithm

  1. d[s] = 0, everything else ∞; put s in a min-heap keyed by d.
  2. Pop the vertex u with the smallest d. Skip it if already settled; otherwise it is settled and d[u] is final.
  3. Relax every edge (u, v); whenever d[v] improves, push (d[v], v). Repeat until the heap is empty.
WHY GREEDY IS SAFE

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.

COST

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).

IN THE WILD

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.

CS 351

Try it: Dijkstra, one settled town at a time

CS 351

Dijkstra in Python, and the one thing it cannot do

 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
  • Stale heap entries are skipped, not deleted: no decrease-key, same O(E log V).
  • Negative edges break it. Sandy ⁠→⁠ Zigzag 1, Sandy ⁠→⁠ Government Camp 3, Government Camp ⁠→⁠ Zigzag −5 (downhill), Zigzag ⁠→⁠ Rhododendron 1. Rhododendron settles at 2; the true answer is −1. Settled must mean final.
  • Negative weights: Bellman-Ford, O(V · E). None: Dijkstra, O(E log V). Unweighted: BFS, O(V + E).
CS 351
// ACTIVITY

Activity: Two Trees

Prim and Dijkstra on one graph, Dijkstra on another, and one question about a negative edge

O(n²)O(n)O(log n)

Two Trees

IN PAIRS, ONE WORKSHEET, BOTH NAMES

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.

  • For Prim, list the crossing edges each time; the lightest one is the answer, and the list is where mistakes hide.
  • For Dijkstra, one row per pop: the vertex, its final distance, and the estimates after relaxing its edges. Settled vertices are never touched again.
625411121097SABCDE
CS 351

Debrief 1 of 3: Prim from S

  • Tree {S}: crossing S–A (6), S–B (2). Take S–B (2).
  • Tree {S, B}: crossing S–A (6), A–B (5), B–C (7), B–D (11). Take A–B (5).
  • Tree {S, A, B}: A–C (4), B–C (7), B–D (11). Take A–C (4).
  • Tree {S, A, B, C}: C–E (10), B–D (11), C–D (12). Take C–E (10).
  • Tree {S, A, B, C, E}: D–E (9), B–D (11), C–D (12). Take D–E (9). Total 30.
625411121097SABCDE

THE MST FROM S: S-B, B-A, A-C, C-E, E-D, WEIGHT 30.

CS 351

Debrief 2 of 3: Dijkstra from S, and the two trees

poppeddSABCDE
S0062∞∞∞
B2062913∞
A6062913∞
C906291319
D1306291319
E1906291319
  • Pop order S, B, A, C, D, E. Shortest paths: A = 6 (direct), B = 2, C = 9 (S–B–C), D = 13 (S–B–D), E = 19 (S–B–C–E).
  • Inside the MST: A = 7, C = 11, E = 21, D = 30 (S–B–A–C–E–D) against a true distance of 13. The two trees share only S–B and C–E.
  • The MST minimizes the total weight of the edges it keeps; a shortest-path tree minimizes each vertex's distance from s. Different objectives, different trees. The heap compares w(u, v) for Prim and d[u] + w(u, v) for Dijkstra.
625411121097SABCDEd = 0d = 6d = 2d = 9d = 13d = 19

THE SHORTEST-PATH TREE FROM S, WITH δ(S, v). COMPARE WITH THE MST.

CS 351

Debrief 3 of 3: Dijkstra on the shuttle

poppeddSPQRT
S0073∞∞
Q3043129
P404369
R604369
T904369
  • Pop order S, Q, P, R, T. Best route S⁠→⁠Q⁠→⁠T, 9 kWh; parents Q←S, P←Q, R←P, T←Q. d[P] dropped from 7 to 4 when Q was popped: a new (4, P) went into the heap and the old (7, P) was popped later and skipped.
  • (f) R⁠→⁠T measured as −4. Dijkstra is no longer guaranteed: it settles a vertex on the assumption that no later path can be shorter, and a negative edge breaks that. Run Bellman-Ford.
  • (g) With the −4, the best route is S⁠→⁠Q⁠→⁠P⁠→⁠R⁠→⁠T = 3 + 1 + 2 − 4 = 2. A negative cycle would be a loop that gains charge forever, so the data would be wrong; Bellman-Ford's check pass reports it.
7312964SQPRTd = 0d = 4d = 3d = 6d = 9

THE SHORTEST-PATH TREE FROM S WITH ALL LEGS NON-NEGATIVE. WITH R ⁠→⁠ T = −4 THE ROUTE TO T CHANGES.

CS 351
// CLOSING

Wrap-up

What to keep, and what comes next

O(n²)O(n)O(log n)

The one-slide version

  • Prim: one growing tree, always the lightest edge leaving it, a heap of candidate edges. O(E log V). Correct by the cut property, like Kruskal.
  • Weighted shortest paths: lightest path, not fewest edges. Negative edges fine, negative cycles make the problem undefined.
  • Relaxation: try to improve d[v] through (u, v). Estimates are never too low.
  • Bellman-Ford: relax every edge |V| − 1 times, then one check pass. O(V · E), negative weights allowed, detects negative cycles.
  • An MST is not a shortest-path tree. Different objective, different tree, even on the same graph.
  • Dijkstra: no negative weights, so relax in order of distance with a heap: each vertex settles once. O(E log V), the same heap as Prim, a different key.
  • Next: dynamic programming. Bellman-Ford was secretly our first DP: d after round k is the best path using at most k edges.
CS 351

References

  1. T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to Algorithms, 4th ed. MIT Press, 2022. Section 21.2 (Prim's algorithm), Chapter 22 introduction, Section 22.1 (Bellman-Ford), and Section 22.3 (Dijkstra).
  2. E. W. Dijkstra, “A note on two problems in connexion with graphs,” Numerische Mathematik, vol. 1, pp. 269–271, 1959.
  3. R. C. Prim, “Shortest connection networks and some generalizations,” Bell System Technical Journal, vol. 36, no. 6, pp. 1389–1401, 1957.
  4. R. Bellman, “On a routing problem,” Quarterly of Applied Mathematics, vol. 16, no. 1, pp. 87–90, 1958.
  5. L. R. Ford Jr., “Network flow theory,” RAND Corporation, Paper P-923, 1956.
CS 351