Union-Find
Maintain disjoint groups as trees of parent pointers so you can merge two groups and test whether two items share a group in near-constant time.
Instinct Items get grouped by "connected", "same as" or "shares a" links and you keep asking which group something is in: merge sets with Union-Find.
Memory hack Find your boss, merge the bosses; squash the chain (path compression) and hang the short tree under the tall one (rank).
At a glance
When to use it
- Items are grouped through connected, friends-with or shares-a links
- You count components or ask whether two items are in the same group
- Edges arrive one at a time and you must react, such as spotting the first edge that closes a cycle
- Records sharing a key such as an email must be merged
Key idea
Represent each group as a tree whose root is its label; find climbs to the root and union links two roots, and with path compression plus union by size each operation is nearly O(1) amortised instead of an O(V + E) traversal per query.
Typical complexity
O(alpha(n)) amortised per operation, effectively O(1), with O(n) space
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Number of Provinces | Medium | Read solution | LeetCode ↗ |
| 2 | Number of Connected Components in an Undirected Graph | Medium | Read solution | LeetCode ↗ |
| 3 | Graph Valid Tree | Medium | Read solution | LeetCode ↗ |
| 4 | Redundant Connection | Medium | Read solution | LeetCode ↗ |
| 5 | Accounts Merge | Medium | Read solution | LeetCode ↗ |
| 6 | Minimize Malware Spread | Hard | Read solution | LeetCode ↗ |
The instinct
Union-Find (Disjoint Set Union, DSU) keeps a forest where every group is a tree and the root is the group's representative. Two operations matter:
find(x): follow parent pointers up to the root.union(a, b): find both roots; if they differ, hang one root under the other.
Reach for it when the problem is about equivalence: "connected", "friends of friends", "same account", "same component". It shines when connections arrive one by one and you must react immediately, for example spotting the first edge that closes a cycle in Redundant Connection.
Recognise it
| Signal in the problem | Why it points here |
|---|---|
| "number of connected components / provinces / friend circles" | Count successful unions: components = n - unions |
| "which edge, if removed, makes the graph a tree" | The first edge whose endpoints already share a root closes a cycle |
| "is this edge list a valid tree?" | Exactly n - 1 edges and no union fails |
| "merge accounts that share an email" | Transitive grouping via shared keys |
| "a == b, b != c: are the equations consistent?" | Union all equalities, then check inequalities |
| Edges or cells added over time, answer after each addition | Dynamic connectivity without re-running DFS |
| "connect all points at minimum cost" | Kruskal's MST uses DSU to skip cycle-forming edges |
Not this pattern if...
- You need the actual path or the shortest distance: BFS/DFS.
- Edges are directed and direction matters (prerequisites): Topological Sort.
- Edges get deleted over time: plain DSU cannot split sets (process offline in reverse if possible).
- One static traversal answers everything and the graph is a grid: DFS is often shorter to write.
Memory hack
Find your boss, merge the bosses. Every person reports to a manager; the top of the chain is the boss. Two teams merge when their bosses shake hands and one reports to the other. Path compression: after you climb the chain once, everyone on it reports directly to the boss. Union by rank/size: the smaller company joins the bigger one so chains stay short.
Analogy: merging clubs. Each club has a president. Asking "are Ana and Raj in the same club?" means asking each for their president and comparing names. Merging clubs means one president agrees to report to the other; members don't need to move.
Say it out loud in the interview:
- "Each element starts as its own set; I'll map non-integer keys (emails, names) to ids."
- "For each relation I union the two;
unionreturns false if they were already together." - "Find uses path compression and union uses size, so each operation is almost O(1)."
How it works
Trace on 5 nodes with edges (0,1), (2,3), (1,3), (0,2) using union by size:
| Edge | Roots found | Action | parent after | Components |
|---|---|---|---|---|
| start | - | - | [0,1,2,3,4] | 5 |
| (0,1) | 0, 1 | 1 under 0 | [0,0,2,3,4] | 4 |
| (2,3) | 2, 3 | 3 under 2 | [0,0,2,2,4] | 3 |
| (1,3) | 0, 2 | 2 under 0 (tie, first wins) | [0,0,0,2,4] | 2 |
| (0,2) | 0, 0 | same root: cycle edge | unchanged | 2 |
The last edge is redundant because 0 and 2 were already connected. Running find(3) later climbs 3 → 2 → 0 and, with path compression, rewires 3 straight to 0.
Grouping by keys (Accounts Merge). Give each email an id, union every email in an account with that account's first email, then bucket emails by their root and sort each bucket.
Choosing a victim (Minimize Malware Spread). Union all edges, compute component sizes, count how many initially infected nodes fall in each component, and prefer removing the infected node that is the only infected one in the largest component.
Java template
import java.util.*; class DSU { private final int[] parent, size; private int components; DSU(int n) { parent = new int[n]; size = new int[n]; components = n; for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; } } int find(int x) { // iterative path compression (halving) while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } boolean union(int a, int b) { // false = already connected (cycle edge) int ra = find(a), rb = find(b); if (ra == rb) return false; if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; } parent[rb] = ra; // smaller tree under larger size[ra] += size[rb]; components--; return true; } boolean connected(int a, int b) { return find(a) == find(b); } int componentSize(int x) { return size[find(x)]; } int components() { return components; } } class DsuUsage { // Map arbitrary keys (emails, names, coordinates) to dense ids first. static int idOf(Map<String, Integer> ids, String key) { return ids.computeIfAbsent(key, k -> ids.size()); } static int[] firstRedundantEdge(int[][] edges) { // nodes labelled 1..n DSU dsu = new DSU(edges.length + 1); for (int[] e : edges) if (!dsu.union(e[0], e[1])) return e; return new int[0]; } }
Complexity
| Operation | Time | Space |
|---|---|---|
find / union with compression + size |
O(α(n)) amortised, effectively constant | O(n) arrays |
find with neither optimisation |
O(n) worst case (a chain) | O(n) |
| Process E edges on V nodes | O(V + E · α(V)) | O(V) |
| Accounts Merge with sorting output | O(N log N) for N emails | O(N) |
α is the inverse Ackermann function, which stays below 5 for any realistic input size.
Variations
- Count components: decrement a counter on each successful union. Example: Number of Provinces, Number of Connected Components in an Undirected Graph.
- Cycle detection in undirected graphs: a failed union means a cycle. Example: Redundant Connection, Graph Valid Tree (also require exactly n - 1 edges).
- Grouping by shared keys: union through a hash map of key → id. Example: Accounts Merge.
- Component statistics: track size per root, then reason per component. Example: Minimize Malware Spread.
- Weighted DSU: store a ratio or offset to the parent to answer "a / b = ?" (Evaluate Division style).
- Kruskal's MST: sort edges by weight and union while skipping cycle edges.
Common mistakes
Writing parent[a] = b instead of parent[find(a)] = find(b) links a non-root node and silently splits or corrupts groups. Always union the roots.
- Comparing
parent[a] == parent[b]to test connectivity; parents are not roots. Comparefind(a) == find(b). - Off-by-one with 1-indexed node labels (Redundant Connection uses 1..n); allocate
n + 1. - Skipping both optimisations, which degrades to O(n) per find on adversarial chains.
- Forgetting to map strings or coordinates to integer ids before using arrays.
- Graph Valid Tree: checking only "no cycle" and forgetting that a forest of several trees is also cycle-free; also require one component.
All practice problems
6 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.