Connect all nodes with the least total cost. MSTs are the backbone of network design β from laying fiber optic cable to clustering data points. Two greedy algorithms, same optimal result, different strategies.
IntermediateA spanning tree of a connected graph is a subset of edges that connects every vertex without forming any cycles. It's called a "tree" because it's connected and acyclic (like a tree data structure), and it "spans" because it includes all vertices. If your graph has V vertices, any spanning tree has exactly V-1 edges β the minimum needed to keep everything connected.
A minimum spanning tree (MST) is the spanning tree with the smallest possible total edge weight. Among all the ways to connect every vertex, it's the cheapest.
Think about laying cable between buildings on a campus. Every building needs a network connection (directly or through other buildings), and you want to minimize the total cable used. Each possible cable run between two buildings has a cost (based on distance, terrain, permits). The MST gives you the cheapest wiring plan.
The concept goes back to Otakar BorΕ―vka in 1926, who was trying to design an efficient electrical network for Moravia (now part of the Czech Republic). Kruskal published his algorithm in 1956, and Prim independently discovered his approach around the same time. These remain the two standard MST algorithms today.
Kruskal's is conceptually the simplest MST algorithm. Sort all edges by weight, lightest first. Process them one by one. For each edge, ask: "Would adding this edge create a cycle?" If no, include it in the MST. If yes, skip it. Stop when you have V-1 edges.
The "would this create a cycle?" check is the key operation. If both endpoints are already in the same connected component, adding the edge creates a cycle. If they're in different components, the edge safely bridges them.
find(x) tells you which group x is in, and union(x, y) merges the groups of x and y. Checking "are u and v in the same component?" is find(u) == find(v). Merging components is union(u, v). With path compression (shortcutting find() to point directly at the root) and union by rank (attaching smaller trees under larger ones), both operations run in nearly O(1) amortized time β specifically O(Ξ±(n)) where Ξ± is the inverse Ackermann function, which is β€ 4 for any input you'll ever encounter.
Prim's grows the MST outward from a starting vertex, one edge at a time. At each step, it picks the cheapest edge that connects the current tree to a vertex not yet in the tree. It's the MST equivalent of Dijkstra's algorithm (in fact, the code is nearly identical β just track edge weights instead of cumulative distances).
Most greedy algorithms don't give optimal results (think: making change with arbitrary coin denominations). MST is special because of the cut property: at every step, taking the minimum weight edge that crosses any cut is provably safe β it must belong to some MST. Both Kruskal's and Prim's exploit this, just in different ways. Kruskal's considers edges globally (lightest first); Prim's considers edges locally (cheapest way to expand the current tree).
Toggle between Kruskal's and Prim's to see how each builds the same MST differently. Kruskal's sorts edges globally and picks the lightest. Prim's grows outward from a starting node. Watch the total weight accumulate as edges join the tree.
| Algorithm | Time | Space | Best For |
|---|---|---|---|
| Kruskal's | O(E log E) | O(V + E) | Sparse graphs, edge list input |
| Prim's (binary heap) | O(E log V) | O(V + E) | Dense graphs, adjacency list input |
| Prim's (adj matrix) | O(VΒ²) | O(VΒ²) | Very dense / complete graphs (E β VΒ²) |
| BorΕ―vka's | O(E log V) | O(V + E) | Parallel implementations |
Since E β€ VΒ² and log(VΒ²) = 2 log V, the complexities of Kruskal's (O(E log E)) and Prim's with a heap (O(E log V)) are essentially the same in big-O terms. The practical difference comes down to input format, constant factors, and whether you already have Union-Find or a heap handy.
BorΕ―vka's algorithm is less commonly taught but worth knowing: it finds the cheapest edge out of each component simultaneously, merges those components, and repeats. It's O(E log V) and naturally parallelizable β each component can find its cheapest edge independently.
class UnionFind:
"""Disjoint Set Union with path compression and union by rank.
Two optimizations that bring operations from O(n) to nearly O(1):
Path compression: During find(), make every node point directly to the root.
Next time, find() is O(1) for that node.
Union by rank: When merging two trees, attach the shorter one under the
taller one. Keeps the tree balanced, limiting height to O(log n).
Together: O(Ξ±(n)) amortized per operation, where Ξ± is the inverse
Ackermann function β effectively β€ 4 for any practical input.
"""
def __init__(self, n):
self.parent = list(range(n)) # Each node is its own root initially
self.rank = [0] * n # Rank β tree height (upper bound)
def find(self, x):
# Path compression: recursively point every node to the root
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
# Union by rank: attach shorter tree under taller tree
rx, ry = self.find(x), self.find(y)
if rx == ry:
return False # Already in same component β edge would create cycle
if self.rank[rx] < self.rank[ry]:
rx, ry = ry, rx # Ensure rx is the taller root
self.parent[ry] = rx
if self.rank[rx] == self.rank[ry]:
self.rank[rx] += 1 # Tie: new root grows taller by 1
return True
def kruskal(V, edges):
"""Kruskal's MST algorithm.
V: number of vertices (labeled 0 to V-1)
edges: list of (weight, u, v) tuples
Returns: (mst_edges, total_weight)
mst_edges: list of (u, v, weight) tuples in the MST
total_weight: sum of all MST edge weights
"""
edges.sort() # Sort by weight β the greedy choice
uf = UnionFind(V)
mst = []
total = 0
for w, u, v in edges:
if uf.union(u, v): # Only add if it doesn't create a cycle
mst.append((u, v, w))
total += w
if len(mst) == V - 1: # MST is complete β V-1 edges
break
# If len(mst) < V-1, graph is disconnected (no spanning tree exists)
return mst, total
import heapq
def prim(graph, start=0):
"""Prim's MST algorithm using a min-heap.
graph: adjacency list {vertex: [(neighbor, weight), ...]}
start: starting vertex (choice doesn't affect the MST)
Returns: (mst_edges, total_weight)
Nearly identical to Dijkstra β the only difference is what we put
in the heap. Dijkstra: cumulative distance from source.
Prim: just the edge weight (we want the cheapest connection, not
the cheapest total path).
"""
visited = set()
mst = []
total = 0
# Heap entries: (weight, from_vertex, to_vertex)
heap = [(0, -1, start)] # Start with a dummy edge to the start vertex
while heap and len(visited) < len(graph):
w, frm, to = heapq.heappop(heap)
if to in visited:
continue # Already in the tree β skip (lazy deletion)
visited.add(to)
if frm != -1: # Skip the initial dummy edge
mst.append((frm, to, w))
total += w
# Push all edges from the newly added vertex
for neighbor, weight in graph[to]:
if neighbor not in visited:
heapq.heappush(heap, (weight, to, neighbor))
return mst, total
function kruskal(V, edges) {
// edges: [[weight, u, v], ...]
edges.sort((a, b) => a[0] - b[0]);
// Simple Union-Find
const parent = Array.from({length: V}, (_, i) => i);
const rank = new Array(V).fill(0);
function find(x) {
if (parent[x] !== x) parent[x] = find(parent[x]);
return parent[x];
}
function union(x, y) {
let rx = find(x), ry = find(y);
if (rx === ry) return false;
if (rank[rx] < rank[ry]) [rx, ry] = [ry, rx];
parent[ry] = rx;
if (rank[rx] === rank[ry]) rank[rx]++;
return true;
}
const mst = [];
let total = 0;
for (const [w, u, v] of edges) {
if (union(u, v)) {
mst.push({u, v, weight: w});
total += w;
if (mst.length === V - 1) break;
}
}
return { mst, total };
}
Telecom companies face this exact problem: connect N cell towers with fiber optic cable, minimizing total cable length. Each possible connection has a cost based on distance, terrain, and permit requirements. The MST gives the cheapest network that connects everyone.
import math
def plan_fiber_network(towers):
"""Plan minimum-cost fiber optic connections between cell towers.
towers: list of (name, x, y) tuples β tower locations
Uses Kruskal's on a complete graph where edge weight = distance.
In a complete graph with N nodes, there are N*(N-1)/2 edges.
Returns: list of connections to build and total cable length.
"""
n = len(towers)
# Generate all possible connections with their distances
edges = []
for i in range(n):
for j in range(i + 1, n):
name_i, xi, yi = towers[i]
name_j, xj, yj = towers[j]
# Euclidean distance = cable length needed
dist = math.sqrt((xi - xj) ** 2 + (yi - yj) ** 2)
edges.append((dist, i, j))
# Run Kruskal's
edges.sort()
uf = UnionFind(n)
connections = []
total_cable = 0
for dist, i, j in edges:
if uf.union(i, j):
connections.append({
"from": towers[i][0],
"to": towers[j][0],
"cable_km": round(dist, 1)
})
total_cable += dist
if len(connections) == n - 1:
break
return connections, round(total_cable, 1)
# Example: 6 cell towers in a city
towers = [
("Downtown", 0, 0),
("Airport", 12, 5),
("University", 3, 8),
("Hospital", 7, 2),
("Mall", 10, 9),
("Stadium", 5, -3),
]
connections, total = plan_fiber_network(towers)
print(f"Total cable needed: {total} km")
print(f"Connections to build ({len(connections)}):")
for c in connections:
print(f" {c['from']:12s} β {c['to']:12s} ({c['cable_km']} km)")
# Without MST, connecting everyone directly would need 15 cables.
# MST uses only 5 cables (V-1) at minimum total distance.
Real network planners add constraints β redundancy requirements (at least 2 paths between critical sites), capacity limits, right-of-way costs. But the starting point is always the MST, with additional edges added for reliability.