DSU model
A disjoint set union (union-find) keeps a collection of elements split into groups, and answers 'are these two in the same group?' and 'merge their groups' in nearly constant time.
Learning goals
- Describe a partition of elements into equivalence classes
- Implement find and union on a parent array
- Count connected components and name each set's representative
- Read a parent array as a forest and detect an invalid one
Prerequisites
Friends of friends are friends: if A knows B and B knows C, they are all in one circle. Whenever you only need to merge circles and ask whether two people share one, a union-find does it without ever storing who knows whom.
Equivalence classes
A relation that is reflexive, symmetric and transitive is an **equivalence relation**, and it splits the elements into **equivalence classes**: every element belongs to exactly one class, and elements are related exactly when they share a class. 'Is connected to' in an undirected graph is such a relation, and so is 'has the same remainder modulo 5'. A union-find represents the classes directly.
The simplest representation, **quick-find**, stores a class label per element. Checking two elements is one comparison, but merging two classes relabels a whole class, O(n). Quick-find is the baseline that the tree representation of the next items improves: merging becomes cheap, and finding becomes a short walk.
1class QuickFind { // each element stores its class label directly2private final int[] label;3QuickFind(int n) {4label = new int[n];5for (int i = 0; i < n; i++) label[i] = i;6}7boolean connected(int a, int b) { return label[a] == label[b]; } // O(1)8void union(int a, int b) { // O(n): relabel a whole class9int from = label[a], to = label[b];10if (from == to) return;11for (int i = 0; i < label.length; i++) if (label[i] == from) label[i] = to;12}13}
Union-find: find, union, path compression, union by size and rank
Interactive · edit the inputdisjoint-set forest (arrow = parent, roots on top) · 6 nodes
parent[x]
6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing.
1int[] parent, size, rank; // parent[x] == x for a root2int findPlain(int x) { while (parent[x] != x) x = parent[x]; return x; } // follow parents to the root3int findCompress(int x) { // path compression: point everyone on the path at the root4if (parent[x] != x) parent[x] = findCompress(parent[x]);5return parent[x];6}7int findHalving(int x) { // path halving: skip one level as you climb8while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }9return x;10}11boolean unionNaive(int a, int b) { // link the first root under the second12int ra = find(a), rb = find(b);13if (ra == rb) return false;14parent[ra] = rb;15return true;16}17boolean unionBySize(int a, int b) { // the smaller tree goes under the larger18int ra = find(a), rb = find(b);19if (ra == rb) return false;20if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; }21parent[rb] = ra; size[ra] += size[rb];22return true;23}24boolean unionByRank(int a, int b) { // the lower-rank root goes under the higher one25int ra = find(a), rb = find(b);26if (ra == rb) return false;27if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; }28parent[rb] = ra;29if (rank[ra] == rank[rb]) rank[ra]++;30return true;31}
Live state
n
6
components
6
Execution trace (dry run table)Show 25 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=6 components=6 | 6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing. |
| 2 | L2 | who=find(0) root=0 | find(0): 0 is its own parent: it is the root. |
| 3 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 4 | L14 | op=0-1 child=0 top=1 components=5 | 0-1: parent[0] = 1: the root of the first element goes under the root of the second. 5 sets left. |
| 5 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 6 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 7 | L14 | op=1-2 child=1 top=2 components=4 | 1-2: parent[1] = 2: the root of the first element goes under the root of the second. 4 sets left. |
| 8 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 9 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 10 | L14 | op=2-3 child=2 top=3 components=3 | 2-3: parent[2] = 3: the root of the first element goes under the root of the second. 3 sets left. |
| 11 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 12 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 13 | L14 | op=3-4 child=3 top=4 components=2 | 3-4: parent[3] = 4: the root of the first element goes under the root of the second. 2 sets left. |
| 14 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 15 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 16 | L14 | op=4-5 child=4 top=5 components=1 | 4-5: parent[4] = 5: the root of the first element goes under the root of the second. 1 set left. |
| 17 | L2 | who=find(0) from=0 to=1 hops=1 | find(0): 0 is not a root, follow parent[0] = 1. |
| 18 | L2 | who=find(0) from=1 to=2 hops=2 | find(0): 1 is not a root, follow parent[1] = 2. |
| 19 | L2 | who=find(0) from=2 to=3 hops=3 | find(0): 2 is not a root, follow parent[2] = 3. |
| 20 | L2 | who=find(0) from=3 to=4 hops=4 | find(0): 3 is not a root, follow parent[3] = 4. |
| 21 | L2 | who=find(0) from=4 to=5 hops=5 | find(0): 4 is not a root, follow parent[4] = 5. |
| 22 | L2 | who=find(0) root=5 depth=5 | find(0): the root is 5, 5 steps up. |
| 23 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 24 | L1 | op=0?5 root of a=5 root of b=5 connected=true | 0?5: the roots are 5 and 5, so 0 and 5 are connected. |
| 25 | L1 | return=1 tallest tree=5 find hops=5 parent changes=0 | 1 set remains. The tallest tree has height 5, and all the finds together followed 5 parent links. |
Find
The tree representation stores a parent array: parent[x] is the parent of x, and a **root** is its own parent. find(x) follows parents until it reaches a root, and returns it: two elements are in the same set exactly when their roots are equal. The cost is the depth of x, which the optimisations of the next lesson keep tiny.
Open the animation with the preset "Chain, naive linking: a tall tree": every hop of find is shown as a step from a node to its parent. A find on the deepest node of a chain of 6 follows 5 parent links, which is why unbalanced trees are slow.
1class Dsu {2final int[] parent;3Dsu(int n) {4parent = new int[n];5for (int i = 0; i < n; i++) parent[i] = i; // every element is its own root6}7int find(int x) {8while (parent[x] != x) x = parent[x]; // follow parents to the root9return x;10}11}
Union-find: find, union, path compression, union by size and rank
Interactive · edit the inputdisjoint-set forest (arrow = parent, roots on top) · 6 nodes
parent[x]
6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing.
1int[] parent, size, rank; // parent[x] == x for a root2int findPlain(int x) { while (parent[x] != x) x = parent[x]; return x; } // follow parents to the root3int findCompress(int x) { // path compression: point everyone on the path at the root4if (parent[x] != x) parent[x] = findCompress(parent[x]);5return parent[x];6}7int findHalving(int x) { // path halving: skip one level as you climb8while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }9return x;10}11boolean unionNaive(int a, int b) { // link the first root under the second12int ra = find(a), rb = find(b);13if (ra == rb) return false;14parent[ra] = rb;15return true;16}17boolean unionBySize(int a, int b) { // the smaller tree goes under the larger18int ra = find(a), rb = find(b);19if (ra == rb) return false;20if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; }21parent[rb] = ra; size[ra] += size[rb];22return true;23}24boolean unionByRank(int a, int b) { // the lower-rank root goes under the higher one25int ra = find(a), rb = find(b);26if (ra == rb) return false;27if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; }28parent[rb] = ra;29if (rank[ra] == rank[rb]) rank[ra]++;30return true;31}
Live state
n
6
components
6
Execution trace (dry run table)Show 25 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=6 components=6 | 6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing. |
| 2 | L2 | who=find(0) root=0 | find(0): 0 is its own parent: it is the root. |
| 3 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 4 | L14 | op=0-1 child=0 top=1 components=5 | 0-1: parent[0] = 1: the root of the first element goes under the root of the second. 5 sets left. |
| 5 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 6 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 7 | L14 | op=1-2 child=1 top=2 components=4 | 1-2: parent[1] = 2: the root of the first element goes under the root of the second. 4 sets left. |
| 8 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 9 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 10 | L14 | op=2-3 child=2 top=3 components=3 | 2-3: parent[2] = 3: the root of the first element goes under the root of the second. 3 sets left. |
| 11 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 12 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 13 | L14 | op=3-4 child=3 top=4 components=2 | 3-4: parent[3] = 4: the root of the first element goes under the root of the second. 2 sets left. |
| 14 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 15 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 16 | L14 | op=4-5 child=4 top=5 components=1 | 4-5: parent[4] = 5: the root of the first element goes under the root of the second. 1 set left. |
| 17 | L2 | who=find(0) from=0 to=1 hops=1 | find(0): 0 is not a root, follow parent[0] = 1. |
| 18 | L2 | who=find(0) from=1 to=2 hops=2 | find(0): 1 is not a root, follow parent[1] = 2. |
| 19 | L2 | who=find(0) from=2 to=3 hops=3 | find(0): 2 is not a root, follow parent[2] = 3. |
| 20 | L2 | who=find(0) from=3 to=4 hops=4 | find(0): 3 is not a root, follow parent[3] = 4. |
| 21 | L2 | who=find(0) from=4 to=5 hops=5 | find(0): 4 is not a root, follow parent[4] = 5. |
| 22 | L2 | who=find(0) root=5 depth=5 | find(0): the root is 5, 5 steps up. |
| 23 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 24 | L1 | op=0?5 root of a=5 root of b=5 connected=true | 0?5: the roots are 5 and 5, so 0 and 5 are connected. |
| 25 | L1 | return=1 tallest tree=5 find hops=5 parent changes=0 | 1 set remains. The tallest tree has height 5, and all the finds together followed 5 parent links. |
Union
union(a, b) merges the sets of a and b: find both roots, and if they differ, make one root the parent of the other. Nothing else changes: no element moves, and one pointer is enough to merge two whole sets. The method returns whether a merge happened, which is exactly the information many problems need (for example 'did this edge connect two components?').
The naive rule below hangs the first root under the second. It is correct but can build a chain, as the preset "Chain, naive linking: a tall tree" shows for 0-1 1-2 2-3 3-4 4-5: the tree ends up with height 5. Choosing which root goes on top, by size or rank, fixes that.
1class Dsu {2final int[] parent;3Dsu(int n) {4parent = new int[n];5for (int i = 0; i < n; i++) parent[i] = i;6}7int find(int x) {8while (parent[x] != x) x = parent[x];9return x;10}11boolean union(int a, int b) { // link the first root under the second12int ra = find(a), rb = find(b);13if (ra == rb) return false; // already in the same set14parent[ra] = rb;15return true;16}17}
Union-find: find, union, path compression, union by size and rank
Interactive · edit the inputdisjoint-set forest (arrow = parent, roots on top) · 6 nodes
parent[x]
6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing.
1int[] parent, size, rank; // parent[x] == x for a root2int findPlain(int x) { while (parent[x] != x) x = parent[x]; return x; } // follow parents to the root3int findCompress(int x) { // path compression: point everyone on the path at the root4if (parent[x] != x) parent[x] = findCompress(parent[x]);5return parent[x];6}7int findHalving(int x) { // path halving: skip one level as you climb8while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }9return x;10}11boolean unionNaive(int a, int b) { // link the first root under the second12int ra = find(a), rb = find(b);13if (ra == rb) return false;14parent[ra] = rb;15return true;16}17boolean unionBySize(int a, int b) { // the smaller tree goes under the larger18int ra = find(a), rb = find(b);19if (ra == rb) return false;20if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; }21parent[rb] = ra; size[ra] += size[rb];22return true;23}24boolean unionByRank(int a, int b) { // the lower-rank root goes under the higher one25int ra = find(a), rb = find(b);26if (ra == rb) return false;27if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; }28parent[rb] = ra;29if (rank[ra] == rank[rb]) rank[ra]++;30return true;31}
Live state
n
6
components
6
Execution trace (dry run table)Show 25 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=6 components=6 | 6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing. |
| 2 | L2 | who=find(0) root=0 | find(0): 0 is its own parent: it is the root. |
| 3 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 4 | L14 | op=0-1 child=0 top=1 components=5 | 0-1: parent[0] = 1: the root of the first element goes under the root of the second. 5 sets left. |
| 5 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 6 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 7 | L14 | op=1-2 child=1 top=2 components=4 | 1-2: parent[1] = 2: the root of the first element goes under the root of the second. 4 sets left. |
| 8 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 9 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 10 | L14 | op=2-3 child=2 top=3 components=3 | 2-3: parent[2] = 3: the root of the first element goes under the root of the second. 3 sets left. |
| 11 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 12 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 13 | L14 | op=3-4 child=3 top=4 components=2 | 3-4: parent[3] = 4: the root of the first element goes under the root of the second. 2 sets left. |
| 14 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 15 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 16 | L14 | op=4-5 child=4 top=5 components=1 | 4-5: parent[4] = 5: the root of the first element goes under the root of the second. 1 set left. |
| 17 | L2 | who=find(0) from=0 to=1 hops=1 | find(0): 0 is not a root, follow parent[0] = 1. |
| 18 | L2 | who=find(0) from=1 to=2 hops=2 | find(0): 1 is not a root, follow parent[1] = 2. |
| 19 | L2 | who=find(0) from=2 to=3 hops=3 | find(0): 2 is not a root, follow parent[2] = 3. |
| 20 | L2 | who=find(0) from=3 to=4 hops=4 | find(0): 3 is not a root, follow parent[3] = 4. |
| 21 | L2 | who=find(0) from=4 to=5 hops=5 | find(0): 4 is not a root, follow parent[4] = 5. |
| 22 | L2 | who=find(0) root=5 depth=5 | find(0): the root is 5, 5 steps up. |
| 23 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 24 | L1 | op=0?5 root of a=5 root of b=5 connected=true | 0?5: the roots are 5 and 5, so 0 and 5 are connected. |
| 25 | L1 | return=1 tallest tree=5 find hops=5 parent changes=0 | 1 set remains. The tallest tree has height 5, and all the finds together followed 5 parent links. |
Connected components
Start with n sets, one per vertex, and process the edges of an undirected graph: every edge that joins two different sets is a merge and lowers the number of components by one. After all edges, the count is the number of connected components. It takes O(E) union-find operations and no adjacency lists.
This is the advantage over a search: edges can arrive one at a time and the count is always current. In the animation's "Provinces: count components" preset, 6 vertices and the edges 0-1 1-2 3-4 2-0 4-5 give 2 components; the edge 2-0 is skipped because 0 and 2 are already connected.
1static int countComponents(int n, int[][] edges) {2int[] parent = new int[n];3for (int i = 0; i < n; i++) parent[i] = i;4int components = n;5for (int[] e : edges) {6int a = e[0], b = e[1];7while (parent[a] != a) a = parent[a];8while (parent[b] != b) b = parent[b];9if (a != b) { parent[a] = b; components--; } // a merge: one component fewer10}11return components;12}
Union-find on edges: components, redundant edge, Kruskal
Interactive · edit the inputdisjoint-set forest (arrow = parent, roots on top) · 6 nodes
edges in the order given
Start with n components, one per vertex. Every edge whose endpoints are in different sets merges two components (count - 1); an edge inside one set changes nothing.
1int countComponents(int n, int[][] edges) {2DSU d = new DSU(n); int comps = n;3for (int[] e : edges) if (d.union(e[0], e[1])) comps--; // a successful union merges two components4return comps;5}6int[] firstCycleEdge(int n, int[][] edges) { // the first edge whose endpoints are already connected7DSU d = new DSU(n);8for (int[] e : edges) if (!d.union(e[0], e[1])) return e;9return null;10}11long kruskal(int n, int[][] edges) { // edges are {a, b, w}12int[][] sorted = edges.clone();13Arrays.sort(sorted, Comparator.comparingInt(e -> e[2])); // stable: ties keep the input order14DSU d = new DSU(n); long total = 0;15for (int[] e : sorted) if (d.union(e[0], e[1])) total += e[2]; // keep an edge only if it joins two components16return total;17}
Live state
vertices
6
edges
5
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | vertices=6 edges=5 | Start with n components, one per vertex. Every edge whose endpoints are in different sets merges two components (count - 1); an edge inside one set changes nothing. |
| 2 | L3 | edge=0-1 components=5 | Edge 0-1 joins the sets of roots 0 and 1: 5 components left. |
| 3 | L3 | edge=1-2 components=4 | Edge 1-2 joins the sets of roots 0 and 2: 4 components left. |
| 4 | L3 | edge=3-4 components=3 | Edge 3-4 joins the sets of roots 3 and 4: 3 components left. |
| 5 | L3 | edge=2-0 root of a=0 root of b=0 | Edge 2-0: both ends have root 0, so they are already connected. Skip it. |
| 6 | L3 | edge=4-5 components=2 | Edge 4-5 joins the sets of roots 3 and 5: 2 components left. |
| 7 | L4 | return=2 | 2 connected components (provinces). The edges kept form a spanning forest with 4 edges. |
Representative
Each set is named by its **representative**, the root of its tree. The name is not meaningful by itself and can change when sets merge, but it is consistent at any moment: all members of a set return the same root. That is enough to group elements, to key a map by set, or to label components.
To list the groups, call find for every element and bucket by the root. The code below returns the representative of every element and the groups in order of their smallest member. Never cache a representative across a union: the old root may have lost its status.
1class Dsu {2final int[] parent;3Dsu(int n) {4parent = new int[n];5for (int i = 0; i < n; i++) parent[i] = i;6}7int find(int x) {8while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }9return x;10}11void union(int a, int b) { parent[find(a)] = find(b); }12int[] representatives() { // the root of every element13int[] rep = new int[parent.length];14for (int i = 0; i < rep.length; i++) rep[i] = find(i);15return rep;16}17Map<Integer, List<Integer>> groups() { // root -> members in increasing order18Map<Integer, List<Integer>> g = new TreeMap<>();19for (int i = 0; i < parent.length; i++) g.computeIfAbsent(find(i), k -> new ArrayList<>()).add(i);20return g;21}22}
Union-find: find, union, path compression, union by size and rank
Interactive · edit the inputdisjoint-set forest (arrow = parent, roots on top) · 6 nodes
parent[x]
6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing.
1int[] parent, size, rank; // parent[x] == x for a root2int findPlain(int x) { while (parent[x] != x) x = parent[x]; return x; } // follow parents to the root3int findCompress(int x) { // path compression: point everyone on the path at the root4if (parent[x] != x) parent[x] = findCompress(parent[x]);5return parent[x];6}7int findHalving(int x) { // path halving: skip one level as you climb8while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }9return x;10}11boolean unionNaive(int a, int b) { // link the first root under the second12int ra = find(a), rb = find(b);13if (ra == rb) return false;14parent[ra] = rb;15return true;16}17boolean unionBySize(int a, int b) { // the smaller tree goes under the larger18int ra = find(a), rb = find(b);19if (ra == rb) return false;20if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; }21parent[rb] = ra; size[ra] += size[rb];22return true;23}24boolean unionByRank(int a, int b) { // the lower-rank root goes under the higher one25int ra = find(a), rb = find(b);26if (ra == rb) return false;27if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; }28parent[rb] = ra;29if (rank[ra] == rank[rb]) rank[ra]++;30return true;31}
Live state
n
6
components
6
Execution trace (dry run table)Show 25 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=6 components=6 | 6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing. |
| 2 | L2 | who=find(0) root=0 | find(0): 0 is its own parent: it is the root. |
| 3 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 4 | L14 | op=0-1 child=0 top=1 components=5 | 0-1: parent[0] = 1: the root of the first element goes under the root of the second. 5 sets left. |
| 5 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 6 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 7 | L14 | op=1-2 child=1 top=2 components=4 | 1-2: parent[1] = 2: the root of the first element goes under the root of the second. 4 sets left. |
| 8 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 9 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 10 | L14 | op=2-3 child=2 top=3 components=3 | 2-3: parent[2] = 3: the root of the first element goes under the root of the second. 3 sets left. |
| 11 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 12 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 13 | L14 | op=3-4 child=3 top=4 components=2 | 3-4: parent[3] = 4: the root of the first element goes under the root of the second. 2 sets left. |
| 14 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 15 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 16 | L14 | op=4-5 child=4 top=5 components=1 | 4-5: parent[4] = 5: the root of the first element goes under the root of the second. 1 set left. |
| 17 | L2 | who=find(0) from=0 to=1 hops=1 | find(0): 0 is not a root, follow parent[0] = 1. |
| 18 | L2 | who=find(0) from=1 to=2 hops=2 | find(0): 1 is not a root, follow parent[1] = 2. |
| 19 | L2 | who=find(0) from=2 to=3 hops=3 | find(0): 2 is not a root, follow parent[2] = 3. |
| 20 | L2 | who=find(0) from=3 to=4 hops=4 | find(0): 3 is not a root, follow parent[3] = 4. |
| 21 | L2 | who=find(0) from=4 to=5 hops=5 | find(0): 4 is not a root, follow parent[4] = 5. |
| 22 | L2 | who=find(0) root=5 depth=5 | find(0): the root is 5, 5 steps up. |
| 23 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 24 | L1 | op=0?5 root of a=5 root of b=5 connected=true | 0?5: the roots are 5 and 5, so 0 and 5 are connected. |
| 25 | L1 | return=1 tallest tree=5 find hops=5 parent changes=0 | 1 set remains. The tallest tree has height 5, and all the finds together followed 5 parent links. |
Parent arrays
The whole structure is one integer array. parent[x] == x marks a root; any other entry is an edge to a parent, so the array describes a **forest** of rooted trees, one per set. A valid parent array has no cycle other than the self-loops of the roots: following parents from any element must reach a root. The animation draws the forest with the array shown above it, so you can read the two views side by side.
To work with a parent array directly, two helpers are useful: the depth of an element (number of parent links to its root) and a validity check that rejects cycles. The check runs in O(n) by colouring: an element is 'in progress' while its path is explored and 'done' once it is known to reach a root.
1static int depth(int[] parent, int x) { // number of parent links from x to its root2int d = 0;3while (parent[x] != x) { x = parent[x]; d++; }4return d;5}6static boolean isForest(int[] parent) { // every element must reach a root without cycling7int n = parent.length;8int[] state = new int[n]; // 0 unvisited, 1 on the current path, 2 known good9for (int s = 0; s < n; s++) {10int x = s;11while (state[x] == 0 && parent[x] != x) { state[x] = 1; x = parent[x]; }12if (state[x] == 1) return false; // we came back to the current path: a cycle13for (int y = s; state[y] == 1; y = parent[y]) state[y] = 2;14state[x] = 2;15}16return true;17}
Union-find: find, union, path compression, union by size and rank
Interactive · edit the inputdisjoint-set forest (arrow = parent, roots on top) · 6 nodes
parent[x]
6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing.
1int[] parent, size, rank; // parent[x] == x for a root2int findPlain(int x) { while (parent[x] != x) x = parent[x]; return x; } // follow parents to the root3int findCompress(int x) { // path compression: point everyone on the path at the root4if (parent[x] != x) parent[x] = findCompress(parent[x]);5return parent[x];6}7int findHalving(int x) { // path halving: skip one level as you climb8while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }9return x;10}11boolean unionNaive(int a, int b) { // link the first root under the second12int ra = find(a), rb = find(b);13if (ra == rb) return false;14parent[ra] = rb;15return true;16}17boolean unionBySize(int a, int b) { // the smaller tree goes under the larger18int ra = find(a), rb = find(b);19if (ra == rb) return false;20if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; }21parent[rb] = ra; size[ra] += size[rb];22return true;23}24boolean unionByRank(int a, int b) { // the lower-rank root goes under the higher one25int ra = find(a), rb = find(b);26if (ra == rb) return false;27if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; }28parent[rb] = ra;29if (rank[ra] == rank[rb]) rank[ra]++;30return true;31}
Live state
n
6
components
6
Execution trace (dry run table)Show 25 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=6 components=6 | 6 elements, each in its own set: parent[x] = x makes every element a root (the set's representative). Union links one root under the other with no balancing. |
| 2 | L2 | who=find(0) root=0 | find(0): 0 is its own parent: it is the root. |
| 3 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 4 | L14 | op=0-1 child=0 top=1 components=5 | 0-1: parent[0] = 1: the root of the first element goes under the root of the second. 5 sets left. |
| 5 | L2 | who=find(1) root=1 | find(1): 1 is its own parent: it is the root. |
| 6 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 7 | L14 | op=1-2 child=1 top=2 components=4 | 1-2: parent[1] = 2: the root of the first element goes under the root of the second. 4 sets left. |
| 8 | L2 | who=find(2) root=2 | find(2): 2 is its own parent: it is the root. |
| 9 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 10 | L14 | op=2-3 child=2 top=3 components=3 | 2-3: parent[2] = 3: the root of the first element goes under the root of the second. 3 sets left. |
| 11 | L2 | who=find(3) root=3 | find(3): 3 is its own parent: it is the root. |
| 12 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 13 | L14 | op=3-4 child=3 top=4 components=2 | 3-4: parent[3] = 4: the root of the first element goes under the root of the second. 2 sets left. |
| 14 | L2 | who=find(4) root=4 | find(4): 4 is its own parent: it is the root. |
| 15 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 16 | L14 | op=4-5 child=4 top=5 components=1 | 4-5: parent[4] = 5: the root of the first element goes under the root of the second. 1 set left. |
| 17 | L2 | who=find(0) from=0 to=1 hops=1 | find(0): 0 is not a root, follow parent[0] = 1. |
| 18 | L2 | who=find(0) from=1 to=2 hops=2 | find(0): 1 is not a root, follow parent[1] = 2. |
| 19 | L2 | who=find(0) from=2 to=3 hops=3 | find(0): 2 is not a root, follow parent[2] = 3. |
| 20 | L2 | who=find(0) from=3 to=4 hops=4 | find(0): 3 is not a root, follow parent[3] = 4. |
| 21 | L2 | who=find(0) from=4 to=5 hops=5 | find(0): 4 is not a root, follow parent[4] = 5. |
| 22 | L2 | who=find(0) root=5 depth=5 | find(0): the root is 5, 5 steps up. |
| 23 | L2 | who=find(5) root=5 | find(5): 5 is its own parent: it is the root. |
| 24 | L1 | op=0?5 root of a=5 root of b=5 connected=true | 0?5: the roots are 5 and 5, so 0 and 5 are connected. |
| 25 | L1 | return=1 tallest tree=5 find hops=5 parent changes=0 | 1 set remains. The tallest tree has height 5, and all the finds together followed 5 parent links. |
Practice ladder
Now try it yourself
Easy
- Find if Path Exists in Graph
- Find the Town Judge
Medium
- Number of Provinces
- Number of Connected Components in an Undirected Graph
Advanced
- Smallest String With Swaps
- Satisfiability of Equality Equations
Key takeaways
- A union-find stores a partition: a parent array whose roots are the set representatives.
- find walks to the root; union links two roots and reports whether anything changed.
- Counting components = n minus the number of successful unions.