Minimum spanning trees
Build minimum spanning trees with Kruskal and Prim, and prove the cut property.
A spanning tree of a connected graph is a subgraph that includes every vertex and just enough edges to keep it connected (exactly for a graph with vertices, and no cycles). The minimum spanning tree (MST) is the spanning tree of smallest total edge weight. It is the cheapest way to wire up a network, build a hierarchy, or cluster points[1]. Two greedy algorithms find it; both are proved correct by the cut property[2].
Spanning trees
For a connected undirected weighted graph with vertices and edges, any spanning tree has exactly edges and no cycles, and connects every vertex. There are usually many spanning trees; the MST is the one minimizing the sum of edge weights. If edge weights are distinct, the MST is unique; with ties, multiple MSTs may share the same total weight.
The cut property
A cut partitions the vertices into two nonempty sets. A crossing edge has one endpoint in each part.
Cut property. For any cut, the minimum-weight crossing edge belongs to every MST.
Proof. Suppose an MST does not contain the minimum-weight crossing edge . Adding to creates a unique cycle, since has edges and is acyclic. That cycle must contain at least one other crossing edge (it leaves at some point and returns, requiring two crossings). Because is the lightest crossing edge, . Replacing with keeps the structure connected and acyclic (same number of edges, the cycle broken), and the total weight does not increase. So is also a spanning tree of weight no greater than , hence also an MST, and it contains .
Both algorithms below are different ways of repeatedly applying the cut property.
Kruskal's algorithm
Build the MST edge by edge, smallest first:
- Sort all edges by weight.
- Iterate through them in order; add an edge if and only if it does not create a cycle.
The cycle check uses a union-find (disjoint-set) data structure: each vertex starts in its own set, and an edge creates a cycle iff and are already in the same set. Union-find with path compression and union by rank supports both operations in nearly constant amortized time, and Kruskal's running time is , dominated by the sort. Each edge added is the lightest in the cut separating its component from everything else, so the cut property applies.
Prim's algorithm
Grow a single tree from a starting vertex:
- Start with one vertex in the tree.
- Repeatedly add the lightest edge crossing from the tree to a vertex outside, using a min-priority queue.
This is Dijkstra's algorithm with "edge weight" replacing "distance from source". The running time is with a binary heap. The cut between the current tree and the rest of the graph is recomputed implicitly at each step, and the lightest crossing edge gets added.
Choosing between them
Kruskal is conceptually simpler (sort and union-find) and works well when edges are already sorted or sorting is cheap, especially on sparse graphs. Prim shines on dense graphs (with a Fibonacci heap it runs in ) and naturally extends to greedy network-growing applications.
Check it on arrays
# Union-Find with path compression and union by rank.
class UF:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # path compression
x = self.parent[x]
return x
def union(self, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry: return False
if self.rank[rx] < self.rank[ry]: rx, ry = ry, rx
self.parent[ry] = rx
if self.rank[rx] == self.rank[ry]: self.rank[rx] += 1
return True
def kruskal(n, edges):
"""edges: list of (weight, u, v)."""
uf = UF(n)
mst, cost = [], 0
for w, u, v in sorted(edges):
if uf.union(u, v):
mst.append((u, v, w)); cost += w
if len(mst) == n - 1: break
return mst, cost
# Sample graph (vertices 0..5)
edges = [
(4, 0, 1), (8, 0, 7),
(8, 1, 2), (11, 1, 7),
(7, 2, 3), (4, 2, 5), (2, 2, 8),
(9, 3, 4), (14, 3, 5),
(10, 4, 5),
(2, 5, 6),
(1, 6, 7), (6, 6, 8),
(7, 7, 8),
]
n = 9
mst, cost = kruskal(n, edges)
print("Kruskal MST cost:", cost)
print("MST edges:", mst)
# Prim's algorithm using a priority queue
import heapq
def prim(n, adj, start=0):
in_tree = [False] * n
pq = [(0, start, -1)] # (weight, vertex, parent)
mst, cost = [], 0
while pq and len(mst) < n - 1:
w, u, p = heapq.heappop(pq)
if in_tree[u]: continue
in_tree[u] = True
if p != -1:
mst.append((p, u, w)); cost += w
for v, wv in adj[u]:
if not in_tree[v]: heapq.heappush(pq, (wv, v, u))
return mst, cost
adj = {i: [] for i in range(n)}
for w, u, v in edges:
adj[u].append((v, w)); adj[v].append((u, w)) # undirected
mst_p, cost_p = prim(n, adj)
print("Prim MST cost: ", cost_p)
print("equal totals:", cost == cost_p)Both algorithms find spanning trees of the same total weight, the cut property guaranteeing they reach the same optimal cost.
Exercises
Exercise 1. Run Kruskal's algorithm by hand on the graph with vertices and edges (weight, endpoints): , , , , . List the edges added in order and the total weight.
Solution
Step 1. Sort edges by weight: , , , , .
Step 2. Process in order, adding an edge only if its endpoints are in different components.
- : components differ. Add it. Components: .
- : and differ. Add it. Components: .
- : and are already joined. Skip (would form a cycle).
- : and differ. Add it. Components: .
- : all joined; the tree has edges. Stop.
Step 3. Edges added: , , . Total weight .
Exercise 2. Using the cut property, prove that if every edge weight is distinct, the MST is unique.
Solution
Step 1. Suppose, for contradiction, that two distinct MSTs and exist. Since they differ, some edge is in one but not the other; let be the minimum-weight edge among all edges in exactly one of . Say .
Step 2. Removing from splits it into two components, defining a cut with crossing it. Adding to creates a cycle; that cycle must contain another edge crossing the same cut, with .
Step 3. Both and cross the cut. By the choice of as the minimum differing edge, (weights are distinct, so the inequality is strict). Then is a spanning tree of strictly smaller weight than , contradicting that is an MST. Hence no two distinct MSTs exist.
Exercise 3 (code). Verify on a random graph that Kruskal and Prim return the same total weight, and that the MST has exactly edges and is connected. Use the implementations from the lesson.
Solution
Equal totals confirm both greedy strategies reach the optimum; the edge count and a union-find connectivity check confirm the result is a spanning tree.
import heapq, random
class UF:
def __init__(self, n):
self.parent = list(range(n)); self.rank = [0] * n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry: return False
if self.rank[rx] < self.rank[ry]: rx, ry = ry, rx
self.parent[ry] = rx
if self.rank[rx] == self.rank[ry]: self.rank[rx] += 1
return True
def kruskal(n, edges):
uf = UF(n); mst, cost = [], 0
for w, u, v in sorted(edges):
if uf.union(u, v):
mst.append((u, v, w)); cost += w
if len(mst) == n - 1: break
return mst, cost
def prim(n, adj, start=0):
in_tree = [False] * n
pq = [(0, start, -1)]; mst, cost = [], 0
while pq and len(mst) < n - 1:
w, u, p = heapq.heappop(pq)
if in_tree[u]: continue
in_tree[u] = True
if p != -1: mst.append((p, u, w)); cost += w
for v, wv in adj[u]:
if not in_tree[v]: heapq.heappush(pq, (wv, v, u))
return mst, cost
rng = random.Random(0)
n = 12
# Build a connected graph: a random spanning path plus extra random edges.
edges = [(rng.randint(1, 20), i, i + 1) for i in range(n - 1)]
for _ in range(20):
u, v = rng.randrange(n), rng.randrange(n)
if u != v: edges.append((rng.randint(1, 20), u, v))
mst_k, cost_k = kruskal(n, edges)
adj = {i: [] for i in range(n)}
for w, u, v in edges:
adj[u].append((v, w)); adj[v].append((u, w))
mst_p, cost_p = prim(n, adj)
# Same optimal cost from both algorithms.
assert cost_k == cost_p
# Exactly V - 1 edges, and they connect all vertices.
assert len(mst_k) == n - 1
uf = UF(n)
for u, v, w in mst_k: uf.union(u, v)
assert len({uf.find(i) for i in range(n)}) == 1
print(cost_k, cost_p, len(mst_k))The final A5 lesson trades exactness for memory: probabilistic data structures.
References
- [1]Kruskal, J. B. (1956). "On the shortest spanning subtree of a graph and the traveling salesman problem". Proceedings of the AMS, 7, 48-50. doi:10.1090/S0002-9939-1956-0078686-7↩
- [2]Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press. Minimum spanning trees, the cut property, and the Kruskal and Prim algorithms (Ch. 23).↩