Disjoint Sets
Track which elements belong to the same group, and merge groups - in nearly per operation.
Learn
- The Idea - the one question DSU answers, the forest of parent pointers, the four arrays, the invariants, and the five things DSU flatly refuses to do. Start here if "union-find" has ever felt like an incantation rather than a structure.
- Find & Union - both operations, path compression and union by rank/size taken separately, why the two together are effectively constant, and a sandbox for switching either off. This is the page to actually internalize.
- Metadata on the Roots -
count,size, and the general rule for hanging any mergeable value on a representative. Where most of the actual answers come from. - Recognising the Pattern - the trigger phrases, DSU versus BFS/DFS, encoding non-integer elements, virtual nodes, running time backwards, and the Kruskal skeleton. The half that is hard: noticing.
- Variants - parity DSU, weighted DSU, rollback DSU, and when each earns its extra field. Read once the plain structure is automatic.
What do I reach for
| The question asks for | Reach for | Cost |
|---|---|---|
| "are these two connected / related / equal" | find(a) == find(b) | O(a(n)) |
| "how many groups / provinces / circles" | dsu.count after absorbing every edge | O(E a(n)) |
"the largest group", "how big is x's group" | size metadata, read as size[find(x)] - Metadata | O(1) per query |
| "which edge closes a cycle / is redundant" | the first union that returns False - the return value | O(E a(n)) |
| "is this a valid tree" | len(edges) == n - 1 and every union returns True | O(E a(n)) |
| "cheapest way to connect everything" | Kruskal: sort, then union - sort then union | O(E log E) |
| "the earliest moment everything is connected" | sort events by time, union until count == 1 | O(E log E) |
| "these two must be in DIFFERENT groups" | parity DSU, or the 2n trick - Variants | O(a(n)) |
| "a is twice b, b is 3x c, what is a/c" | weighted DSU - Variants | O(a(n)) |
| "after each REMOVAL, how many components" | run the events backwards - reverse time | O(E a(n)) |
| the same, but each query depends on the last answer | rollback DSU - Variants | O(log n) per op |
| the path or the distance between two things | not DSU - use BFS/DFS | O(V + E) |
"can a reach b" on a directed graph | not DSU - use SCC | O(V + E) |
The bug checklist
Every trap on the learn pages, in the order they tend to bite. If a DSU solution is wrong and you do not know why, read down this list.
| # | Check | Symptom when wrong |
|---|---|---|
| 1 | Does find assign the compressed parent - self.parent[x] = self.find(...)? | Correct answers, zero speedup, timeout only on the large test. |
| 2 | Does union attach the two roots, not the two arguments? | A group silently splits. Counts drift; nothing errors. |
| 3 | Is count -= 1 inside the "roots differ" branch? | Component count too low by the number of duplicate edges. |
| 4 | Is size read as size[find(x)], never as a bare size[x]? | Plausible-but-small sizes. Passes small inputs. |
| 5 | Is the DSU sized for the real element count (2 * len(pairs) when interning)? | IndexError, or a needlessly enormous array. |
| 6 | Grid flattening: is it r * cols + c and not r * rows + c? | Cells collide on non-square grids; unrelated groups merge. |
| 7 | Is the relation genuinely symmetric? Is the graph directed? | You compute weak connectivity and never find out. |
| 8 | Does any(...) / all(...) short-circuit before every edge was absorbed? | The boolean is right and dsu.count afterwards is wrong. |
| 9 | Are you bucketing members by find(i) rather than parent[i]? | A component splits into several groups in the final pass. |
| 10 | Does the problem remove anything? Did you reverse the event order? | You start looking for a split operation that does not exist. |
| 11 | Parity / weighted DSU: is the update between the recursion and the rewiring? | Connectivity stays correct; only the side/ratio answers are wrong. |
| 12 | Rollback DSU: is path compression off? | Rollback restores a state that was never true. |
| 13 | Recursive find on a 10^5-node chain - could it exceed ~1000 frames? | RecursionError. Use the iterative path-halving form. |
Implementation
The version to write from memory. Explained line by line in Find & Union.