Kruskal and Prim
The two minimum-spanning-tree algorithms you will actually implement. Kruskal grows a forest edge by edge, cheapest first, using a union-find structure to reject cycle-closing edges; Prim grows one tree outward from a root with a priority queue, exactly Dijkstra rekeyed by attachment cost.
╌╌╌╌
This builds on Minimum Spanning Trees, which set up the problem and proved the cut property (a light edge crossing a respecting cut is safe) and cycle property (a heaviest edge on a cycle is droppable). Everything below rests on those two certificates. The running example is the same nine-town graph from that lesson, whose unique MST has total weight .
Kruskal's algorithm
's strategy is global and edge-centric: consider the edges in order of increasing weight, and add each one unless it would form a cycle. The set is a forest (an acyclic graph) that gradually coalesces into a single tree once is fully connected.
Why is each added edge safe? It reduces directly to the cut property.
Each rejection is justified too: a rejected edge closes a cycle on which it is a maximum-weight edge (everything already accepted is no heavier), so by the cycle property some MST omits it.
The cycle test, are and already in the same tree?
, reduces to the
question : does adding
keep the graph acyclic? This is what the
disjoint-set (union-find) data
structure answers efficiently.1 It supports ,
(, which component is in?), and (merge two
components).
- 1
- 2foreach vertex do
- 3call
- 4sort the edges of into nondecreasing order by weight
- 5foreach edge in sorted order do
- 6if thenstays acyclic
- 7safe edge
- 8call
- 9return
A full trace, union-find state included
Running on the nine-town graph makes the accept/reject rhythm visible. The thirteen edges in nondecreasing order (ties broken alphabetically) are
The table shows, for every edge scanned, the union-find test and the partition of into components after the step:
| # | edge | same component? | action | partition afterwards | |
|---|---|---|---|---|---|
| 1 | – | no | accept | ||
| 2 | – | no | accept | ||
| 3 | – | no | accept | ||
| 4 | – | no | accept | ||
| 5 | – | no | accept | ||
| 6 | – | no | accept | ||
| 7 | – | no | accept | ||
| 8 | – | yes | reject | unchanged | |
| 9 | – | yes | reject | unchanged | |
| 10 | – | no | accept | ||
| 11 | – | yes | reject | unchanged | |
| 12 | – | yes | reject | unchanged | |
| 13 | – | yes | reject | unchanged |
Eight accepts, total weight — the MST from the opening figure. Once the eighth edge lands (step 10) the forest is spanning and the loop could stop early; steps 11–13 can only reject. Four snapshots of the growing forest:
Making it fast: union by size
The simplest implementation keeps an array where names 's current component. Then is , but a naive that relabels one whole side costs in the worst case. The fix is the classic union-by-size argument:
- Alongside , keep , a linked list of the vertices currently in component , so we can enumerate a component cheaply.
- On , relabel the smaller component: choose the side with and rewrite for the vertices in that smaller side.
Why this wins: whenever a vertex has its label rewritten, it sat in the smaller of the two merged components, so the component containing at least doubles in size. After relabellings of , its component holds at least vertices; since no component exceeds , , i.e. . Summing over vertices, the total work spent updating across all unions is
The bound is loose in practice. In the trace above, the eight unions relabel smaller sides of sizes : ten label rewrites total, nowhere near .
Running time. The pieces are: sorting, since implies ; the calls at each; and total union work. Sorting dominates:
The pointer-based union-find (union by rank plus path compression) does all operations in , where is the inverse Ackermann function, at most for any input that fits in the physical universe. That refinement only matters when sorting is not the bottleneck: if the edges arrive pre-sorted, or the weights are small integers that a counting or radix sort handles in , Kruskal's total drops to , effectively linear.
Prim's algorithm
's strategy is local and vertex-centric: grow a single tree outward from an arbitrary root, repeatedly attaching the cheapest edge that links a tree vertex to a non-tree vertex. Here the set is always one connected tree.2
Safety is again the cut property.
This is exactly with the relaxation rule changed. Where Dijkstra keys a frontier vertex by (distance from the source), Prim keys it by alone (cost to attach to the finished set). Prim keeps every non-tree vertex in a min-priority queue keyed by , maintaining the invariant
Extracting the minimum yields the next vertex to absorb; absorbing it may make its neighbors cheaper to reach, so we relax their keys with .
- 1foreach vertex do
- 2
- 3
- 4start tree at
- 5min-PQ keyed by
- 6
- 7while do
- 8cheapest to attach
- 9if then
- 10commit safe edge
- 11foreach adjacent to with do
- 12if then
- 13's best link to tree
- 14Decrease-Key
- 15return
A single step in isolation: the tree has been grown from root ; every frontier vertex carries a key equal to its cheapest edge into . The cut respects the tree, and returns the endpoint of the lightest crossing edge — here – at weight — which the cut property certifies as safe:
A full trace, queue state included
Now the whole run, from root on the nine-town graph. Each row lists the vertex extracted, the tree edge committed, the calls its absorption triggers, and the queue contents afterwards (vertices with key omitted):
| step | extracted | edge added | key updates ( in parentheses) | queue after: : key |
|---|---|---|---|---|
| 1 | (key ) | — | (), () | |
| 2 | () | – | (), () | |
| 3 | () | – | (), () | |
| 4 | () | – | (), () | |
| 5 | () | – | (), () | |
| 6 | () | – | — | |
| 7 | () | – | () | |
| 8 | () | – | () | |
| 9 | () | – | — | empty |
Reading the table against the invariant: at every step the extracted key is the weight of the lightest edge crossing the cut around the current tree, and the committed edge is that light edge. 's key falls as the tree grows toward it, and 's from to the moment joins; a vertex's key only ever decreases. The same eight edges as Kruskal arrive in a different order — Kruskal took – first, Prim last but one — because the two algorithms apply the cut property to different cuts. Three snapshots of the growing tree, with each frontier vertex's cheapest attachment drawn dashed:
Running time. Prim performs operations and up to operations, so in general . With a binary heap all three operations cost , giving , the same as Kruskal. With a Fibonacci heap, drops to amortized while stays , improving the total to
When the graph is dense, , this is , that is, linear, and asymptotically the best known. On very dense graphs there is an even simpler route to the same bound: skip the heap, keep as a plain array, and find each minimum by scanning it. That is scans at plus per key update, for total — on a complete graph () this plain-array Prim is linear in the input size and beats the heap version's .
Edge cases and pitfalls
Equal weights. Nothing in the cut-property proof requires distinct weights (it only uses ), so Kruskal and Prim are correct as stated under ties; the MST just may not be unique, and different tie-breaks yield different, equally cheap trees. The one algorithm that needs care is Borůvka's, where an inconsistent tie-break can create a cycle within a single round — hence the fixed total order in the remark above. If you need the MST to be well defined (say, for hashing or comparison), perturb ties by edge index: order by lexicographically.
Negative weights are harmless. Every spanning tree has exactly edges, so adding a constant to every weight adds exactly to every spanning tree's cost and preserves their relative order. MST algorithms only ever compare weights. Contrast Dijkstra, where shifting weights breaks correctness precisely because paths have different edge counts.
An MST is not a shortest-path tree. The MST minimizes one global sum; it guarantees nothing about the path between any particular pair. In the nine-town graph the MST joins to through ––––– at cost , while the direct edge – costs . Building the MST and then reading distances off it is a classic error.
What an MST does guarantee about paths. It minimizes the bottleneck:
This is why MSTs answer minimax questions: the path between and inside an MST minimizes the maximum edge weight over all – paths. Two of the practice problems (Path With Minimum Effort, Swim in Rising Water) amount to this bottleneck property in disguise.
Heaviest edge, again. The cycle property removes the heaviest edge on each cycle, not the heaviest edge of the graph; a heavy bridge survives every MST (the figure in the cycle-property section). Symmetrically, a maximum spanning tree needs no new theory: negate all weights, or flip the comparisons.
Disconnected input. If is not connected no spanning tree exists. Kruskal degrades gracefully: it returns the minimum spanning forest (an MST of each component) with no code change, since it never needed connectivity. Prim must be restarted once per component, as its single tree can never cross between them.
Which to use?
Throughout, and .
| Borůvka | Kruskal | Prim | |
|---|---|---|---|
| Grows | every component at once | a forest, globally | a single tree, from a root |
| Core structure | component scan / union-find | union-find (union by size) | priority queue |
| Headline time | |||
| Best variant | parallel rounds | work after sort | (Fib. heap) |
| Best when | parallel / distributed | edges already sorted; sparse | dense; adjacency-rich data |
The tie-breakers in practice: Kruskal wins on sparse graphs and whenever the
edges come pre-sorted or sort in linear time (integer weights), since what
remains is near-linear union-find work; it also handles edge lists with no
adjacency structure and disconnected inputs directly. Prim wins on dense
graphs, especially with the plain-array variant at , and on implicitly
dense inputs like connect these points in the plane
where materializing and
sorting all candidate edges just to feed Kruskal is the expensive
part. Borůvka wins when the work must be split across machines or threads,
and its rounds compose with the other two: run a few Borůvka rounds to shrink
the graph, then finish with Prim or Kruskal — the fastest practical schemes are
hybrids of exactly this shape.
All three are correct for the same reason, the cut property, and all three are greedy algorithms whose greediness is provably optimal — rare for greedy strategies.
What MSTs are used for
Beyond network design, the MST is a building block in data analysis and approximation.
Single-linkage clustering. Run Kruskal but stop early, after edges: the remaining components are the clusters that single-linkage agglomerative clustering would form.3 Merging the two closest clusters is precisely accepting the next-cheapest Kruskal edge, so the whole MST is the clustering dendrogram, and cutting it at a chosen height gives any number of clusters for free. Deleting the heaviest MST edges partitions the points into the groups that maximize the smallest inter-cluster gap — the cut and bottleneck properties at work on real data.
Approximating hard tours. The metric travelling-salesman problem is NP-hard, but the MST gives a fast -approximation: build the MST, walk it in a depth-first order (a full traversal crosses each edge twice, costing ), and shortcut past repeats. Since the optimal tour minus one edge is a spanning tree, , so the shortcut tour costs at most . Christofides' refinement adds a minimum matching on the odd-degree vertices to reach a -approximation — still the classic guarantee for metric TSP, and the MST is its foundation.
The list runs long: MSTs underlie network design and broadcast trees, image segmentation (Felzenszwalb-Huttenlocher runs single-linkage on a pixel grid), the taut-string structure of point clouds, and the bottleneck
routing problems from the practice set. Whenever a problem asks to connect everything cheaply, to find the widest-gap partition, or to approximate a metric optimization, the MST is the standard starting point.
Takeaways
- A minimum spanning tree connects all vertices of a connected weighted
graph with exactly edges of least total weight;
tree
means connected and acyclic, no root. - The generic method adds one safe edge at a time, maintaining the invariant that the growing edge set sits inside some MST; after additions the invariant forces equality.
- The cut property is the inclusion certificate: a light edge crossing a cut that respects the current edge set is always safe. The exchange argument must swap out the edge on the cycle created by adding ; swapping an arbitrary crossing edge is a tempting mistake.
- The cycle property is the exclusion certificate: a heaviest edge on any cycle can be dropped, and a strictly heaviest one is in no MST. It applies only to cycle edges; bridges are in every spanning tree. Distinct weights make the MST unique.
- adds every component's cheapest exit edge each round; the component count at least halves, giving rounds and total, with naturally parallel rounds.
- adds edges cheapest-first, skipping cycle-forming ones, using union-find; sorting dominates at , and union by size keeps the relabelling work to because each vertex's component can double only times.
- is rekeyed by attachment cost: grow one tree from a start vertex, attaching the lightest crossing edge via a priority queue; with a binary heap, with a Fibonacci heap, with a plain array — linear on dense graphs either way.
- MSTs tolerate negative weights and ties, minimize the bottleneck edge on every path, and are not shortest-path trees.
Footnotes
- CLRS, Ch. 21 — Data Structures for Disjoint Sets — union by size/rank and path compression, with the bound. ↩
- Skiena, §6 — Weighted Graph Algorithms — Prim's algorithm growing one tree via a priority queue. ↩
- Skiena, §6 — Weighted Graph Algorithms — single-linkage clustering as the minimum spanning tree cut at a chosen number of components. ↩
╌╌ END ╌╌