Data structures & algorithms3 min
Minimum Spanning Trees
Imagine you are building a power grid for 10 cities. You have the costs to lay electrical wire between various pairs of cities (a Weighted, Undirected Graph). Your goal is to connect ALL 10 cities together using the absolute minimum total cost of wire.
This is the Minimum Spanning Tree (MST) problem.
- Spanning: It must connect all vertices together.
- Tree: It must not contain any cycles (a cycle means you wasted money building redundant wire).
- Minimum: The sum of the edge weights must be the lowest possible.
There are two incredibly famous algorithms to solve this.
1. Kruskal's Algorithm
Kruskal's is a Greedy Algorithm that relies entirely on sorting and the Disjoint Set (Union-Find) data structure.
How it Works:
- Extract ALL edges from the graph and sort them in ascending order of their weight.
- Iterate through the sorted edges one by one (picking the cheapest wires first).
- Check if the edge's two nodes are already connected using
UnionFind.connected(u, v). - If they are already connected, ignore the edge (adding it would create a cycle!).
- If they are not connected, add the edge to the MST and merge them using
UnionFind.union(u, v). - Stop when you have added $V - 1$ edges (a tree with V nodes always has exactly V-1 edges).
// Assuming UnionFind class from previous module exists
function kruskals(edges: {u: number, v: number, w: number}[], numVertices: number): number {
// 1. Sort edges by weight
edges.sort((a, b) => a.w - b.w);
const uf = new UnionFind(numVertices);
let mstCost = 0;
let edgesAdded = 0;
// 2. Iterate through sorted edges
for (const edge of edges) {
if (!uf.connected(edge.u, edge.v)) {
// 3. If no cycle, add to MST
uf.union(edge.u, edge.v);
mstCost += edge.w;
edgesAdded++;
// Stop early if we built the complete tree
if (edgesAdded === numVertices - 1) break;
}
}
return mstCost;
}
- Time Complexity: $O(E \log E)$ heavily dominated by the initial sorting of the edges. The Union-Find operations take near $O(1)$ time.
2. Prim's Algorithm
While Kruskal's sorts all edges globally, Prim's algorithm starts from a single node and "grows" the MST outwards, conceptually similar to Dijkstra's algorithm.
It uses a Min-Heap (Priority Queue) to always pick the cheapest edge connecting the growing MST to a node currently outside the MST.
How it Works:
- Pick any random starting vertex and mark it as part of the MST.
- Add all of its connected edges to a Min-Heap.
- Extract the cheapest edge from the Min-Heap.
- If the edge leads to a node already in the MST, discard it (avoids cycles).
- If the edge leads to an unvisited node, add the node to the MST, add the edge's cost to the total, and push all of the new node's edges into the Min-Heap.
- Repeat until all vertices are visited.
function prims(graph: Map<number, {node: number, w: number}[]>, numVertices: number): number {
const visited = new Set<number>();
const pq = new MinPriorityQueue(); // Extracts {node, w} based on min w
let mstCost = 0;
// Start at an arbitrary node (e.g., node 0)
visited.add(0);
for (const edge of graph.get(0) || []) {
pq.enqueue(edge, edge.w);
}
while (!pq.isEmpty() && visited.size < numVertices) {
const edge = pq.dequeue().element;
if (visited.has(edge.node)) continue; // Skip if already in MST
// Add to MST
visited.add(edge.node);
mstCost += edge.w;
// Add new neighbors to Priority Queue
for (const nextEdge of graph.get(edge.node) || []) {
if (!visited.has(nextEdge.node)) {
pq.enqueue(nextEdge, nextEdge.w);
}
}
}
return mstCost;
}
- Time Complexity: $O(E \log V)$ using an Adjacency List and Min-Heap.
Which one to use?
- Kruskal's is generally easier to implement and performs exceptionally well on Sparse Graphs (few edges) because sorting $E$ is fast.
- Prim's performs better on Dense Graphs (many edges) because it doesn't need to globally sort a massive list of edges upfront.