Intuition The one core idea
A Disjoint Set Union is just a bunch of groups where each group elects one "boss" node, and every other node stores an arrow pointing (eventually) up to that boss. Everything else — find, union, rank, path compression, even the scary α ( n ) — is just cleverness about keeping those arrow-chains short so that "who is your boss?" is answered almost instantly.
Before you can read the parent note, you need to see every piece it silently assumes. We build them one at a time, each on top of the last. Nothing is used before it is drawn.
The most basic object here is a node — think of it as a labelled dot. We have n of them, numbered 0 , 1 , 2 , … , n − 1 .
parent[]
A node is one element (one dot) in our world. The whole structure is stored in ONE array called parent. The value parent[x] is the single arrow leaving node x : it names the node that x points to.
x is a root when parent[x] == x — its arrow loops back to itself.
Why an array? Because a node is just a number 0 … n − 1 , so parent is a lookup table: "give me a node number, I give you where it points." That single array is the entire data structure.
Look at the picture: every dot has exactly one outgoing arrow (that is the rule — one parent each). The dot whose arrow curls back on itself is the boss.
Definition Initialization — the starting state
Before anything happens, every node is its own boss — n separate one-node sets. So for every x in 0 … n − 1 we set:
for x in range (n):
parent[x] = x # each node is its own root
rank[x] = 0 # a lone node has height 0
Picture n isolated dots, each with a tiny self-loop arrow. rank[x] = 0 is honest here: a single node really has height 0 . Every later operation grows outward from this clean slate.
If every node has exactly one parent arrow, and you keep following arrows, you always end at a root (the arrows can't loop anywhere else — only the root loops on itself). That shape has a name.
A rooted tree is a group of nodes where each node points to exactly one parent, and following parents always leads up to a single root . The root is the "top". Here, one tree = one set, and the root = that set's representative (its boss/leader).
A forest is simply a collection of separate rooted trees — several bosses, none pointing at each other. That is exactly our whole DSU at any moment: many disjoint trees living side by side, one tree per group. Right after initialization the forest is n tiny one-node trees. (More background: Trees and Forests .)
Intuition Why a tree and not a list?
We could store each set as a flat list of members. But then merging two sets and asking "same group?" would mean scanning lists. A tree lets us answer "which group?" by climbing to the one root — and lets us merge two groups by moving one single arrow (point one root at the other). That is the whole reason trees are the right picture.
Definition Height of a tree
The height is the length of the longest arrow-chain from any node up to the root — i.e. the most arrows you might have to follow to reach the boss.
Walk the figure. On the left is a tall stringy tree: node 3 points to 2, 2 points to 1, 1 points to root 0. To answer "who is 3's boss?" you must follow 3 arrows — the height is 3 . On the right is a bushy tree: four nodes all point directly at root R, so any find is 1 arrow — height 1 . Same number of nodes, wildly different cost. This is the villain of the whole story: a straight-line tree (height = n − 1 ) makes a single find cost n − 1 hops. The two optimizations in the parent note exist entirely to keep this height tiny — short + bushy = good, tall + stringy = bad.
Now we can read the first operation. find(x) means: start at x , follow parent arrows until you hit a root, return that root.
find(x)
Returns the root (representative) of the tree containing x . In words: "who is x 's boss?" The test for "have I arrived?" is exactly parent[x] == x.
Why do we need it at all? Because "are x and y in the same group?" becomes simply find(x) == find(y) — compare the two bosses. Two people are teammates if and only if they salute the same captain.
Definition Path compression — rewriting the arrows as you climb
While climbing to the root, we also repoint every node we pass so it aims straight at the root . Next time, those nodes reach the boss in one hop.
def find (x):
root = x
while parent[root] != root: # step 1: climb to the true root
root = parent[root]
while parent[x] != root: # step 2: rewrite every node on the path
parent[x], x = root, parent[x]
return root
Step 1 finds the root by following arrows until one loops on itself. Step 2 walks the same path again, and for each node sets parent[node] = root — flattening the chain. The parent note's shorter recursive version does the same thing; the two-pass form above just makes the "rewrite every visited pointer" idea explicit.
Look at the figure: before , 5→4→0 is a chain; after a single find(5), both 5 and 4 point directly at root 0. We paid a little extra now to make every future find cheap — that is exactly why the cost is called amortized (§7).
union(x, y)
Merge the set containing x with the set containing y . Mechanically: find both roots, and point one root's arrow at the other root. That single arrow re-parent joins two whole trees.
The picture shows the entire operation: we do NOT touch any leaf; we only redirect one boss to bow to another boss. This is why a tree beats a list — a merge is one pointer change. This same "join two groups" motion is what powers Kruskal's Minimum Spanning Tree and Connected Components .
But which root bows to which? And what if x and y are already in the same group? Here is the full procedure.
Definition Union by rank — the full recipe
def union (x, y):
rx, ry = find(x), find(y)
if rx == ry: # GUARD: already same set
return # do nothing — no self-loop, no bogus rank++
if rank[rx] < rank[ry]: # make rx the taller (or equal) root
rx, ry = ry, rx
parent[ry] = rx # hang the shorter tree under the taller
if rank[rx] == rank[ry]: # only a TIE can make the tree grow
rank[rx] += 1
How the ranks decide: compare rank[rx] and rank[ry].
If they differ, the larger-rank root wins and the smaller bows to it. The combined height stays = max of the two heights, so no rank changes .
If they are equal , either may be the winner; hanging one equal tree under the other adds one level, so we bump the winner's rank by 1 .
if rx == ry: return guard is not optional
Feels harmless: merging a set with itself seems like a no-op anyway.
What actually happens without it: you would run parent[ry] = rx when ry == rx, writing parent[rx] = rx (a needless write) or — worse in variants — wrongly incrementing rank[rx], corrupting the height promise. Always early-return on equal roots.
rank
rank[x] is a number stored only at roots. It is an upper bound on the height of that root's tree — a promise that the tree is no taller than rank[x]. It starts at 0 (see initialization) and only ever goes up , only on a tie during union.
Intuition Why not just store the real height?
Real height changes every time path compression flattens the tree — recomputing it would be expensive. rank is a lazy over-estimate: we never bother correcting it downward. We only ever compare two ranks to decide "which tree is (probably) taller, so which should bow to which?" An approximate answer is good enough for that decision, and cheap to keep.
So rank is not a measurement — it is a tie-breaking hint for union that is always at least the true height. Never recompute it downward.
The parent note talks about O ( n ) , O ( log n ) , O ( α ( n )) . These describe how the work grows as n grows.
O ( f ( n )) (Big-O)
A ceiling on running time, ignoring constant factors. O ( n ) = "cost grows in step with the number of nodes." O ( log n ) = "cost grows like the number of times you can halve n " — much slower. O ( 1 ) = "roughly fixed, no matter how big n is."
Definition Amortized cost
The average cost per operation across a whole sequence of operations, not the worst single one. A single find might occasionally be slow, but because it flattens the tree as it goes (§4), it makes all future finds cheaper — so the average stays tiny. See Amortized Analysis .
Intuition Why "amortized" is the honest word here
Path compression is like paying off a big cleanup once, then coasting cheaply for a long time. Judging it by that one expensive cleanup is unfair; judging it by the average over the whole run is honest — and that average is α ( n ) .
A ( m , n ) — what the two arguments mean
The Ackermann function takes two inputs. Think of the first argument m as choosing which operation , and n as how many times to apply it — each higher m is a wildly more explosive operation than the last:
A ( 0 , n ) = n + 1 , A ( m , 0 ) = A ( m − 1 , 1 ) , A ( m , n ) = A ( m − 1 , A ( m , n − 1 ) )
Reading it: m = 0 just adds one; each step up in m turns the previous operation into repeating it (add → multiply → power → tower → …). So raising m even a little makes the output unimaginably large . That is why A grows faster than any tower of exponentials. (More: Ackermann Function .)
α ( n ) — the inverse
Because A shoots up so violently, its inverse α ( n ) — "how big must the arguments be to reach n ?" — creeps up almost never. It stays ≤ 4 for every input you could ever store in this universe:
α ( n ) ≤ 4 for all n ≤ 2 2 2 2 16
Why does this monster show up? Because the exact cost of path-compression-plus-rank, when analysed carefully, turns out to match the inverse of Ackermann's growth. In practice, treat α ( n ) as "a constant ≤ 4 " — that is the whole point.
path compression flattens
union x y joins two roots
rank = height upper bound
amortized cost over sequence
alpha n inverse Ackermann
DSU with path compression + union by rank
Read it bottom-up to the box DSU: nodes make trees, trees form a forest with one root each, roots enable find/union, height forces us to invent rank, cost language plus amortized analysis produce the α ( n ) guarantee — and all of it feeds the parent topic. Related uses branch off toward Graph Cycle Detection and Connected Components .
Test yourself — cover the right side and answer out loud.
How do you initialize a fresh DSU on nodes 0 … n − 1 ? For every x : parent[x] = x and rank[x] = 0 — each node is its own root, height 0 .
What does parent[x] == x mean? x is a root — the representative/boss of its set.
What is the single array that stores the entire DSU? parent[], where parent[x] is the one arrow leaving node x .
What is a forest, in this context? A collection of separate rooted trees — one tree per disjoint set, no root pointing at another.
Why store each set as a tree, not a flat list? A merge is one pointer change and "same set?" is one climb to the root, instead of scanning lists.
What is a tree's height, in plain words? The longest chain of parent-arrows from any node up to the root — the worst-case number of hops.
What does find(x) return and how does it know it's done? The root of x 's tree; it stops when parent[x] == x.
What does path compression do during a find? Repoints every node on the climbed path directly at the root, so future finds are one hop.
In union, how do the ranks decide who bows to whom? The larger-rank root wins; the smaller bows to it. On a tie, either wins and its rank goes up by 1.
Why must union early-return when find(x) == find(y)? They're already one set; skipping the guard risks a self-loop write or a bogus rank increment that corrupts the structure.
Why is rank always ≥ the true height? union bumps rank exactly when height rises; path compression only lowers height and never touches rank — so rank can only over-estimate.
What does "amortized O ( α ( n )) " mean? Averaged over a whole sequence of operations, each costs about α ( n ) ≤ 4 — effectively constant.
What do the two arguments of A ( m , n ) represent? m picks how explosive the operation is (add, multiply, power, …); n is how many times to apply it.
Why does α ( n ) stay ≤ 4 in practice? It inverts Ackermann's insanely fast growth, so it grows insanely slowly.