Trees and heaps · 15 min

Union-find: same root, same group

Disjoint sets as trees in one parent array: why two elements share a set exactly when they reach the same root, why union links roots and never elements, and how union by rank and path compression make each operation nearly constant, amortised.

01Why it matters

Can these two reach each other yet?

A building is being wired one cable at a time, each cable joining two computers. After every cable someone asks: can computer 3 now reach computer 8 through any chain of cables? Searching from 3 each time walks the cables laid so far, again and again.

It is really a question about groups. Computers that can reach each other form a group, and a new cable can only merge two groups, or do nothing if its ends already share one. Union-find tracks exactly that. It answers "same group?" and merges groups in nearly constant time, averaged over many operations, using just two arrays of numbers.

02Key parts

The pieces you will see

disjoint sets
Groups that share no elements, so every element is in exactly one set. Union-find is also called a disjoint-set structure.
parent and root
Each element stores one number, parent[x]. Following parents from any element ends at the root of its set, the one element that is its own parent. The root names the set.
find and connected
find(x) climbs parent links from x to the root and returns it. connected(a, b) asks whether find(a) and find(b) are the same root.
union
union(a, b) merges the set holding a with the set holding b: it finds both roots and makes one the parent of the other.
rank and path compression
Two tricks that keep trees flat. A root's rank is an upper bound on its tree's height, and union by rank hangs the root of lower rank under the other. Path compression points every element a find climbs past straight at the root.

04Predict

Your turn: roots, hops and ranks

Two sets of five: 0 is the root of 0 to 4, with 3 under 2, and 5 is the root of 5 to 9, with 8 under 7, both of rank 2. Union 3 and 8, predicting at each step whether they already share a set, which root each find reaches, how many pointers compression changes and which root goes underneath. Then ask Connected? for 4 and 9.

Make 6 predictions: step forward and answer each question before the next step plays.

0 of 6 made

05Try it

Union-find in code, and where it pays

In code, union-find is two lists of numbers. find_root (findRoot in JavaScript and TypeScript) is recursive, as in the recursion lesson: a root is the base case, and any other element gets its parent's root, stores it as its own parent and returns it. Those stores, made as the calls return, are path compression. (The visualiser makes them in a second loop instead.) union finds both roots, stops if they match, and otherwise hangs the lower-ranked root under the other.

The program starts from two sets: 0 is the root of 0 to 3, with 3 under 2, and 4 is the root of 4 and 5. The list is forest at the top and parent inside the functions, two names for one list. Each real merge returns 1, so groups counts the sets left.

def find_root(parent, x):
    if parent[x] != x:
        parent[x] = find_root(parent, parent[x])
    return parent[x]

def union(parent, rank, a, b):
    ra = find_root(parent, a)
    rb = find_root(parent, b)
    if ra == rb:
        return 0
    if rank[ra] < rank[rb]:
        ra, rb = rb, ra
    parent[rb] = ra
    if rank[ra] == rank[rb]:
        rank[ra] += 1
    return 1

forest = [0, 0, 0, 2, 4, 4]
rank = [2, 0, 1, 0, 1, 0]
groups = 2
groups -= union(forest, rank, 3, 5)
print(groups)

The link goes between roots, and that matters more than it looks.

The classic user is Kruskal's algorithm for a minimum spanning tree, the cheapest set of edges connecting every node of a weighted graph. It takes edges cheapest first and keeps one only if its ends are not yet connected; otherwise it would close a cycle.

Play Kruskal's algorithm on nodes 0, 1 and 2: union 0 and 1, then 1 and 2, then ask Connected? for 0 and 2. The second union climbs from 1 to its root, 0, and hangs 2 (rank 0) under 0 (rank 1), so rank[0] stays 1. Both finds then reach 0: an edge between 0 and 2 would close a cycle, so Kruskal's algorithm skips it.

Counting a graph's connected components is the same loop: start with n sets, union the ends of every edge, and subtract one for each union that links two roots, as groups did. Either way, the check must compare roots, not parents.

How fast is it in the end? With both tricks, any m operations on n elements take O(m α(n)) time in total, where α, the inverse Ackermann function, is at most 4 for any n you could ever store. That is nearly constant per operation, but only on average over the sequence, and that caveat matters.

Union-find only merges, though: it has no operation that splits a set apart again.

06Check yourself

Check yourself

  1. Question 1 of 5

    From singletons 0 to 3, union(0, 1), union(2, 3) and union(1, 3) run with union by rank, b’s root going under a’s on a tie. Which element ends up as the root, with what rank?

    Your answer comes back in Review in a few days, so it sticks.

  2. Question 2 of 5

    In one set, 0 is the root of 0, 1 and 2; in another, 3 is the root of 3 and 4. Which single write merges them into one set?

  3. Question 3 of 5

    parent is [0, 0, 0, 2] after union(0, 1), union(2, 3) and union(0, 2). Kruskal’s algorithm now looks at the edge between 1 and 3. What should it do?

  4. Question 4 of 5

    A union always sets parent[find(a)] = find(b), with no ranks and no compression. From singletons 0 to 4 it runs union(0, 1), union(0, 2), union(0, 3) and union(0, 4). How many links does find(0) follow now?

  5. Question 5 of 5

    Union by rank and path compression make m operations on n elements take O(m α(n)) time. What does that say about a single find?

07Explain it back

Say it in your own words

How does union-find tell whether two elements are in the same set, and how does it merge two sets? What do union by rank and path compression each do to keep it fast?

08Recap

What to remember

  • Union-find keeps disjoint sets as trees in one parent array. A root is its own parent and names its set.
  • find climbs to the root, and connected(a, b) is find(a) = find(b): compare roots, never parents.
  • union(a, b) links the two roots. Overwriting a non-root's parent splits its set instead.
  • Union by rank keeps every tree at most log₂ n tall; path compression points each path a find climbs at the root.
  • Together, m operations take O(m α(n)): nearly constant each, amortised. Sets merge but never split.
  • Next: minimum spanning trees, where Kruskal's algorithm uses union-find to skip edges that would close a cycle. Practise with Count Connected Groups and The Extra Edge, or open the union-find visualiser to try more operations.
Back to path