A Bit of History
The Union-Find data structure was introduced by Bernard Galler and Michael Fischer in a short 1964 paper on compiler design, motivated by tracking equivalence classes of program variables. Over the following decade, researchers including M. D. McIlroy, Robert Morris, and independently J. D. Tritter contributed the "union by rank/size" and "path compression" optimizations you'll use here. But the full story of why those optimizations are so effective wasn't nailed down until 1975, when Robert Tarjan (already a familiar name from Parts 6 and 8) proved that a sequence of \(m\) operations on \(n\) elements takes \(O(m \, \alpha(n))\) time, where \(\alpha\) is the inverse Ackermann function — a function that grows so slowly that \(\alpha(n) \leq 4\) for any \(n\) you could ever physically enumerate, even though it is not, strictly speaking, a constant.
Working Principle
Union-Find (Disjoint Set Union, DSU) maintains a partition of elements into disjoint groups and supports two operations: find(x) — which group is \(x\) in? (represented by a canonical "representative" element) — and union(x, y) — merge \(x\)'s and \(y\)'s groups. The naive implementation represents each group as a tree (via parent pointers), with find walking up to the root and union attaching one root under the other.
Analogy: Corporate Mergers and Org Charts
Picture each group as a company's org chart, with everyone ultimately reporting up to one CEO (the representative). find(employee) asks "who is this person's ultimate CEO?" by walking up the reporting chain. union(companyA, companyB) is a merger: one CEO now reports to the other. Naively, repeated lopsided mergers can create a very tall, thin org chart where finding the CEO takes a long walk — exactly the degenerate case the two optimizations below prevent.
Two Optimizations That Change Everything
Union by rank (or size): when merging two groups, always attach the shorter (or smaller) tree under the taller (or larger) tree's root, never the reverse. This alone bounds tree height at \(O(\log n)\), since a tree can only grow taller by merging with an equally tall tree, at most \(\log n\) times.
Path compression: every time find(x) walks up to the root, re-point every node visited along the way directly to that root. Future find calls on any of those nodes become instant. This is a self-optimizing data structure — it gets flatter, and faster, the more it's used.
flowchart LR
subgraph Before["Before find(D)"]
A1["A (root)"] --- B1["B"] --- C1["C"] --- D1["D"]
end
subgraph After["After find(D): path compressed"]
A2["A (root)"] --- B2["B"]
A2 --- C2["C"]
A2 --- D2["D"]
end
Together, Not Separately
Either optimization alone already guarantees \(O(\log n)\) amortized time per operation. It's specifically the combination of both — proven by Tarjan in 1975 — that pushes the bound all the way down to \(O(\alpha(n))\), the inverse Ackermann function. This is one of the most celebrated results in the analysis of algorithms precisely because the two simple, independently-obvious tricks interact to produce a bound far better than either alone.
The Inverse Ackermann Bound
The Ackermann function \(A(m,n)\) grows faster than any primitive recursive function — faster than exponentials, faster than towers of exponentials. Its inverse, \(\alpha(n)\), therefore grows unimaginably slowly: \(\alpha(n) \leq 4\) for every \(n\) up to more digits than there are atoms in the observable universe. In every practical sense, \(O(\alpha(n))\) is "constant time" — but it is a genuinely different (and strictly larger) complexity class than true \(O(1)\), which is exactly why the distinction earned its own theorem rather than being waved away.
$$\text{Time (m operations, n elements): } O(m \cdot \alpha(n)) \qquad \text{Space: } O(n)$$
Implementation
Real-World Applications
Image Segmentation and Percolation Simulation
Beyond powering Kruskal's algorithm's cycle check, Union-Find is the standard tool for connected-component labeling in image processing (union adjacent same-region pixels, then read off each final group as one labeled region) and for simulating percolation in physics and materials science (randomly "open" cells in a grid one at a time, union each newly opened cell with its already-open neighbors, and ask "when does a path connect the top row to the bottom row?" — exactly a dynamic connectivity query Union-Find answers in near-constant time per step).
Exercises
- Implement Union-Find with path compression but without union by rank (always attach the second tree under the first), and construct a sequence of unions that produces a long chain — measure how much slower
findbecomes. - Explain why
count(the number of remaining groups) can be maintained in \(O(1)\) extra work per successfulunioncall, and why it's useful for efficiently answering "are all elements now in one group?" - Trace through the percolation case study: build a small 3×3 grid, "open" cells one at a time in some order, and determine after each step whether the top row is connected to the bottom row.
- Challenge: Implement union by size instead of union by rank (track subtree size, attach the smaller tree under the larger) and prove it gives the same \(O(\log n)\) height bound as union by rank, even though it tracks a different quantity.
Limitations
No Splitting, No Undo
Union-Find supports merging groups efficiently, but has no efficient way to split a group back apart or undo a union — path compression actively destroys the information needed to reverse a merge. Applications needing both merges and deletions (fully dynamic connectivity, where edges can be removed as well as added) require fundamentally different, more complex data structures (e.g., link-cut trees or Euler-tour trees), which trade away Union-Find's simplicity for that extra capability.