Minimum Spanning Trees

Week 13, Wednesday — Prim’s Algorithm

April 2, 2026

Announcements

Warm-up: Graph Representations

Adjacency list(you’ve seen this)

graph = {
  "A": [("B",8), ("C",2), ("D",4)],
  "B": [("A",8), ("C",7), ("E",2)],
  "C": [("A",2), ("B",7), ("D",1),
        ("E",3), ("F",9)],
  "D": [("A",4), ("C",1), ("F",5)],
  "E": [("B",2), ("C",3)],
  "F": [("C",9), ("D",5)],
}

Space: \(O(V + E)\)

Iterate neighbors of \(v\): \(O(\deg(v))\)

Check if edge \((u,v)\) exists: \(O(\deg(u))\)

Adjacency matrix(new today)

A B C D E F
A 8 2 4
B 8 7 2
C 2 7 1 3 9
D 4 1 5
E 2 3
F 9 5

Space: \(O(V^2)\)

Iterate neighbors of \(v\): \(O(V)\)

Check if edge \((u,v)\) exists: \(O(1)\)

Muddy City

You can pave road segments. Each segment costs its number of paving stones.

Goal: pave the __________ stones so every house can reach every other.

 

Which roads do you pave?

How many stones total?

 

(CS Unplugged activity)

Minimum Spanning Tree

8 2 4 7 2 1 3 9 5 A B C D E F

Input: connected, undirected graph \(G\) with edge weights

Output: subgraph \(G'\) such that:

  • \(G'\) spans \(G\) (contains all vertices)
  • \(G'\) is connected and acyclic (a tree)
  • \(G'\) has minimum total weight among all spanning trees

 

What is the MST of this graph?

Total weight = ____

Cycle Property

8 2 4 7 2 1 3 9 5 A B C D E F blue = MST

What’s true about every cycle in this graph?

 

Theorem:   ___________________________________

 

 

 

Proof:   ______________________________________

 

 

 

Non-MST Edge Lower Bound

8 2 4 7 2 1 3 5 ? A B C D E F blue = MST  |  orange = edge under scrutiny

Edge C–F is not in the MST. Using the theorem you just derived — what is the minimum possible weight for C–F?

 

MST path C → F:   _________________

 

Min weight of C–F:   _____

 

 

Theorem:   ___________________________________

(for any non-MST edge)

 

MST vs. Shortest Paths

8 2 4 7 4 1 5 9 5 A B C D E F blue = MST (total weight 17)

MST minimizes total edge weight — the cost of connecting everything.

 

Shortest path minimizes the distance between two specific nodes.

 

These are different objectives.

Partition Property (Cut Property)

S = {A, C, D} 8 2 4 7 2 1 3 9 5 A B C D E F green = min crossing edge (always in MST)

Partition Property (Cut Property)

Split vertices into any two sets \(S\) and \(V \setminus S\).

The minimum-weight edge crossing the cut is always in some MST.

Prim’s Algorithm

2 3 4 4 5 4 2 4 4 3 4 3 2 3 3 3 5 4 3 2 A B C D E F G H I J

MST total weight = ____

BFS Revisited

from collections import deque

def bfs(graph, start):
    visited = {start}
    queue = deque([start])

    while queue:
        v = queue.popleft()
        for w in graph[v]:          # examine each neighbor
            if w not in visited:
                visited.add(w)
                queue.append(w)

What type is graph? What type is graph[v]?

Running time? (in terms of \(|V|\) and \(|E|\))

  • Each vertex enters the queue at most ____ time(s).
  • Each edge is examined at most ____ time(s).
  • Total: \(O(\)____\()\)
graph neighbors
A B, D, E
B A, E, C
C B, D
D A, C
E A, B

From BFS to Prim’s

The bag determines what the algorithm optimizes:

Bag Explores by Algorithm
Queue (FIFO) distance from source (hops) BFS
Priority queue (min edge weight) cheapest edge to tree Prim’s MST
Priority queue (min total distance) shortest path from source Dijkstra’s SSSP

 

One change turns BFS into Prim’s:

Replace the queue with a Priority Queue keyed by edge weight. Instead of “which node did I discover first?”, ask “which unvisited node is cheapest to connect to the tree?”

Prim’s Algorithm: Pseudocode

Initialize:
for each v:  d[v] ← ∞,  p[v] ← null
d[s] ← 0
Q ← min-PQ of all vertices, keyed by d
while Q not empty:

  v ← Q.removeMin()

  for each neighbor w of v:

    if w ∈ Q and cost(v,w) < d[w]:
      d[w] ← cost(v,w)
      p[w] ← v
      Q.decreaseKey(w, d[w])

return p
2 8 7 5 7 9 8 4 3 A B C D E F

Prim’s Algorithm: Design Choices

Initialize:
for each v:  d[v] ← ∞,  p[v] ← null
d[s] ← 0
Q ← min-PQ of all vertices, keyed by d
while Q not empty:
  v ← Q.removeMin()
  for each neighbor w of v:
    if w ∈ Q and cost(v,w) < d[w]:
      d[w] ← cost(v,w)
      p[w] ← v
      Q.decreaseKey(w, d[w])

return p

What design choices do we need to make?

 

Put a ⭐ by __________________ operations.

 

Put a 💜 by __________________ operations.

Prim’s Algorithm: Runtime Analysis

Initialize:
for each v:  d[v] ← ∞,  p[v] ← null
d[s] ← 0
Q ← min-PQ of all vertices, keyed by d
while Q not empty:
  v ← Q.removeMin()          ⭐
  for each neighbor w of v:  💜
    if w ∈ Q and cost(v,w) < d[w]:
      d[w] ← cost(v,w)
      p[w] ← v
      Q.decreaseKey(w, d[w])  ⭐

return p


\(n\) removeMins + \(\leq m\) decreaseKeys
💜 \(O(m)\) total (adj list)  |  \(O(n^2)\) total (adj matrix)

Adj matrix Adj list
heap \(O(n^2 + m \log n)\) \(O(m \log n)\)
unsorted array \(O(n^2)\) \(O(n^2)\)

 

Which is best? Depends on graph density:

  • Sparse (\(m=O(n)\)):
  • Dense (\(m=O(n^2)\)):

Prim’s Algorithm: Python

import heapq

def prim(graph, start):
    """graph: {vertex: [(neighbor, weight), ...]}"""
    d = {v: float('inf') for v in graph}   # cheapest edge to reach v
    parent = {v: None for v in graph}
    d[start] = 0
    pq = [(0, start)]                       # (edge_weight, vertex)
    visited = set()

    while pq:
        cost, v = heapq.heappop(pq)
        if v in visited:
            continue
        visited.add(v)

        for w, weight in graph[v]:
            if w not in visited and weight < d[w]:
                d[w] = weight              # just the edge weight (not cumulative!)
                parent[w] = v
                heapq.heappush(pq, (weight, w))

    return parent   # MST as parent pointers