Union-Find (Disjoint Set Union)
Tracks a partition of elements into disjoint sets with near-constant-time find and union, using path compression and union by rank.
Definition
Union-Find maintains a collection of disjoint sets over elements 0…n-1 and supports two operations: find(x) returns a canonical representative of the set containing x, and union(x, y) merges the sets containing x and y. Two elements are in the same set iff find(x) == find(y).
Each set is a rooted tree stored in a parent[] array; the root is the representative. Two heuristics — path compression (point every visited node straight at the root during find) and union by rank/size (attach the shorter tree under the taller) — bring the amortized cost per operation to O(α(n)), where α is the inverse Ackermann function, ≤ 4 for any conceivable n.
It is the backbone of Kruskal's Algorithm, the fastest way to answer incremental connectivity ("after adding these edges, are u and v connected?"), and the standard tool for Cycle Detection in undirected graphs, Connected Components counting, and equivalence-class grouping (accounts merge, similar strings, equations satisfiability).
Intuition
A mental model before the formal terms.
Every person starts as the leader of their own one-person club. When two clubs merge, one leader agrees to report to the other. To find out which club someone belongs to, follow the chain of "who do you report to?" until you hit someone who reports to themselves — the leader.
Path compression: once you have walked the chain, tell everyone on it who the leader is so next time they answer immediately. Union by rank: when merging, the smaller hierarchy joins under the bigger one so chains never get long. Together, chains stay so short that each question is effectively instant.
How it works
- Init:
parent[i] = i,rank[i] = 0(orsize[i] = 1),count = nsets. - find(x): walk
x → parent[x]untilparent[r] == r. On the way back, setparent[node] = rfor every node visited (path compression). Iterative variant: path halving —parent[x] = parent[parent[x]]at each step. - union(x, y):
rx = find(x),ry = find(y). If equal, returnfalse(already connected — this is the cycle signal). Otherwise attach the root with smaller rank under the other; if ranks tie, pick either and increment its rank. Decrementcount. Returntrue. - connected(x, y):
find(x) == find(y). - Union by size is an equivalent alternative: attach the smaller set under the larger and add sizes; it also gives you set sizes for free.
- Extensions: store per-set aggregates at the root (sum, min, count of edges), or a weighted DSU that tracks the offset from each node to its root for "x is d more than y" constraints.
Why it works
Union by rank guarantees a root of rank r has at least 2^r descendants, so tree height is ≤ log₂ n even without compression, giving O(log n) per operation.
Path compression flattens trees aggressively; Tarjan proved the combination yields O(m · α(n)) total for m operations, where α(n) ≤ 4 for n < 10^600. Practically constant.
Because union returns false exactly when both endpoints already share a root, processing an undirected graph's edges in order detects the first edge that closes a cycle.
Operations
| Operation | Description | Cost |
|---|---|---|
| find(x) | Return the root representative of x, compressing the path. | O(α(n)) amortized |
| union(x, y) | Merge the sets of x and y by rank; return false if already merged. | O(α(n)) amortized |
| connected(x, y) | find(x) == find(y). | O(α(n)) amortized |
| count() | Number of disjoint sets. | O(1) |
| size(x) | Size of the set containing x (union by size variant). | O(α(n)) amortized |
| reset / add element | Append a new singleton. | O(1) |
Recognition
How to tell a problem wants this.
- "Are
uandvconnected?" after a sequence of edge additions (never removals). - "Number of connected components / provinces / groups / islands" with edges given as pairs.
- "Merge accounts / equivalent characters / similar groups" — any equivalence relation.
- Minimum spanning tree (Kruskal's Algorithm) or "redundant connection" / cycle in an undirected graph.
- Offline queries that can be processed in sorted order (e.g. "number of pairs reachable with edge weight ≤ k").
Interactive demo
Play, step, change the input. ← → and space work too.
| root | rank | members |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 2 | 0 | 2 |
| 3 | 0 | 3 |
| 4 | 0 | 4 |
| 5 | 0 | 5 |
| 6 | 0 | 6 |
| 7 | 0 | 7 |
1find(x): while parent[x] != x: x = parent[x] # walk to the root2 path compression: point every node on the path directly at the root3union(a, b): ra = find(a); rb = find(b)4 if ra == rb: already same set5 attach the root with smaller rank under the other (union by rank)6 if ranks are equal: rank[new root] += 1Pseudocode
1init: parent[i] = i; rank[i] = 0; count = n2find(x): while parent[x] != x: parent[x] = parent[parent[x]]; x = parent[x]; return x3union(x, y):4 rx, ry = find(x), find(y); if rx == ry: return false5 if rank[rx] < rank[ry]: swap(rx, ry)6 parent[ry] = rx; if rank[rx] == rank[ry]: rank[rx] += 17 count -= 1; return trueImplementation
1class UnionFind:21 · State — parent, rank and set size per node3 def __init__(self, n: int):4 self.parent = list(range(n)) # every node is its own root5 self.rank = [0] * n6 self.size = [1] * n7 self.count = n8 92 · Find with path compression (iterative, two passes)10 def find(self, x: int) -> int:11 root = x12 while self.parent[root] != root:13 root = self.parent[root]14 while self.parent[x] != root: # second pass: point every node on the path at the root15 self.parent[x], x = root, self.parent[x]16 return root17 183 · Union by rank19 def union(self, x: int, y: int) -> bool:20 rx, ry = self.find(x), self.find(y)21 if rx == ry:22 return False # already in the same set23 if self.rank[rx] < self.rank[ry]:24 rx, ry = ry, rx # attach the shorter tree under the taller25 self.parent[ry] = rx26 self.size[rx] += self.size[ry]27 if self.rank[rx] == self.rank[ry]:28 self.rank[rx] += 129 self.count -= 130 return True31 324 · Queries33 def connected(self, x: int, y: int) -> bool:34 return self.find(x) == self.find(y)35 36 def set_size(self, x: int) -> int:37 return self.size[self.find(x)]list(range(n))is the identity parent list;[0] * nand[1] * ninitialise rank and size.findwalks to the root, then the tuple assignmentself.parent[x], x = root, self.parent[x]compresses the path in one line — the right-hand side is evaluated before either target is assigned.unionswapsrx, rywith tuple unpacking so the taller tree stays the root.- Rank grows only on equal-rank merges;
countis decremented on each successful union. set_sizereadssizeat the root found byfind.
- Python has no stdlib disjoint-set;
networkx.utils.UnionFindexists if you already depend on networkx. - Iterative
findmatters more in Python: the default recursion limit is 1000 and recursive calls are slow. - Tuple assignment evaluates the whole right side first, which is what makes the one-line path-compression step correct.
- For hashable non-integer keys, replace the lists with
dicts and usesetdefault(x, x)on first sight.
- Writing the compression as two separate statements in the wrong order (
x = self.parent[x]before rewritingparent[x]) so the original parent is lost. - Naming the method
unionon asetsubclass — clashes withset.union. - Using recursion for
findand hittingRecursionErroron adversarial chains before compression.
unionis a keyword in C++, so the C++ method isunite; JS/TS/Python can useunionfreely.- Recursive
findis idiomatic in C++ (deep stacks are cheap) but risky in Python (recursion limit 1000) and JS (engine-dependent limit around 10k frames); the iterative two-pass form is safe everywhere. - Python tuple assignment lets path compression be a one-liner; C++/JS/TS need a temporary
next. - Only C++ has a fixed-width
int; JS/TS use doubles (safe up to 2^53) and Python ints are unbounded — irrelevant for indices but relevant if you store aggregated weights per set.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | — | — | No positional access. |
| Search | O(α(n)) | O(log n) | Find representative; amortized α(n). |
| Insert | O(1) | O(1) | Add a singleton. |
| Delete | — | — | Not supported without rollback/offline techniques. |
| Update | O(α(n)) | O(log n) | Union. |
| Find | O(α(n)) | O(log n) | |
| Union | O(α(n)) | O(log n) | |
| Connected | O(α(n)) | O(log n) | |
| Count sets | O(1) | O(1) | |
| Space | O(n) | α(n) ≤ 4 for all practical n; worst case per single call is O(log n) with union by rank. | |
Advantages & disadvantages
- Near-constant amortized time per operation with ~20 lines of code.
- Tiny memory footprint: two integer arrays.
- Processes edges online — no need to know the whole graph in advance.
- Naturally reports cycles and component counts as a by-product.
- No un-union: deleting an edge requires rebuilding or an offline/rollback variant (without path compression).
- Cannot enumerate the members of a set without an auxiliary structure.
- Only models undirected connectivity — direction and path lengths are lost.
- Recursive
findcan overflow the stack on10^6-element chains before compression kicks in; use the iterative form.
Use cases
- Kruskal's Algorithm minimum spanning tree.
- Counting Connected Components in a graph given as an edge list.
- Redundant Connection: the first edge whose union fails.
- Accounts Merge, Similar String Groups, Satisfiability of Equality Equations.
- Percolation, image segmentation, and Kruskal-style clustering.
- Offline dynamic connectivity and "number of islands II" (incremental grid additions).
- Dynamic connectivity with only additions of edges.
- Counting components or detecting a cycle in an undirected graph given as edges.
- Kruskal's MST.
- Grouping by an equivalence relation (merge accounts, equal-equations).
- Offline processing of queries sorted by threshold.
- Edges are deleted — DSU cannot split sets; use link-cut trees, offline divide-and-conquer, or recompute.
- You need paths or distances, not just connectivity — use Breadth-First Search (BFS) / Dijkstra's Algorithm.
- The graph is directed — DSU ignores direction; use Tarjan's SCC Algorithm / Kosaraju's Algorithm for strong connectivity.
- You need to list the members of a component — pair with a map from root to member list, or use DFS.
Alternatives
Common mistakes
- Skipping both heuristics — a plain linked chain makes
findO(n). - Comparing
x == yinstead offind(x) == find(y)to test connectivity. - Setting
parent[y] = xinstead ofparent[find(y)] = find(x)— attaches a non-root and corrupts the forest. - Recursive
findwithout compression on large inputs — stack overflow in Python. - Forgetting to map arbitrary labels (strings, emails) to
0…n-1indices first. - Using DSU on a directed graph to answer reachability.
Interview patterns
- Number of Connected Components / Provinces:
countafter unions. - Redundant Connection: first edge whose
unionreturnsfalse. - Accounts Merge: union emails, then group by root.
- Min Cost to Connect All Points: Kruskal.
- Number of Islands II: incremental grid cells with union of neighbors.
- Longest Consecutive Sequence: union
xwithx + 1and track set sizes. - Satisfiability of Equality Equations: union
==pairs, then check every!=.
- Number of IslandsIntermediate