Union-Find

Track groups that keep merging and answer "are these two connected?" almost instantly, with union by size and path compression. Traced, in JS and Python.

16 minLesson 1 of 4
On this page
  1. The idea: every group has a boss
  2. Trace it on six computers
  3. Two tricks that keep trees flat
  4. The code
  5. Common mistakes
  6. Check yourself
  7. What to practise next

You look after a small office network of six computers, numbered 0 to 5. All day, someone plugs in a new cable between two of them. In between, people ask you things like "can computer 5 talk to computer 1 yet?"

You could answer each question with a BFS or DFS from computer 5. That works, but it walks the whole network every time. With 100,000 computers and 100,000 questions, that is billions of steps. You do not need paths here. You only need to know which group each computer is in, and to merge two groups when a cable joins them. A union-find structure does exactly that, and each operation takes only a few steps.

The idea: every group has a boss

Each group picks one member to be its root, like a team captain. Every computer stores one number: its parent, the computer it reports to. A root reports to itself. At the start, nobody is connected, so everyone is their own root.

That gives you two operations:

  • find(x): follow parent pointers from x until you reach a computer that is its own parent. That is the root of x's group. Two computers are connected exactly when they have the same root.
  • union(a, b): find the root of each. If the roots differ, make one root report to the other. One pointer change merges two whole groups.

Notice that union never touches the other members. Everyone who reported to the old root still does, and the old root now reports to the new one, so their find still ends at the right place, just one hop later.

Trace it on six computers

The array is parent: cell i holds the parent of computer i. The arrows in the picture point from each computer to its parent. Each root also keeps a size: how many computers are in its group.

Plug in cables and merge groupsUse the arrows or ← → keys
find(x):  if parent[x] != x: parent[x] = find(parent[x])  return parent[x]union(a, b):  ra, rb = find(a), find(b)  if ra == rb: return false  if size[ra] < size[rb]: swap ra, rb  parent[rb] = ra; size[ra] += size[rb]
  1. parent
    00
    11
    22
    33
    44
    55
    012345
    groups
    6

    1No cables yet. Every computer is its own root, so parent[i] = i and every group has size 1.

  2. parent
    00
    10
    22
    33
    44
    55
    012345
    groups
    5
    size[0]
    2

    2Cable 0–1. The roots are 0 and 1, both size 1. On a tie we keep ra, so 1 now reports to 0.

  3. parent
    00
    10
    22
    32
    44
    55
    012345
    groups
    4
    size[0]
    2
    size[2]
    2

    3Cable 2–3. Same story: 3 now reports to 2.

  4. parent
    00
    10
    22
    32
    44
    54
    012345
    groups
    3
    size[0]
    2
    size[2]
    2
    size[4]
    2

    4Cable 4–5. Now 5 reports to 4. Three groups of two.

  5. parent
    00
    10
    22
    32
    44
    54
    012345
    ra
    0
    rb
    2

    5Cable 1–3. Neither is a root, so first find their roots: 1 reports to 0, and 3 reports to 2. The roots differ, so the groups must merge.

  6. parent
    00
    10
    20
    32
    44
    54
    012345
    groups
    2
    size[0]
    4
    size[4]
    2

    6Both roots have size 2, a tie, so root 2 reports to root 0. One pointer moved, and four computers are now one group. Computer 3 is two hops from its root.

  7. parent
    00
    10
    20
    30
    44
    54
    012345
    groups
    2
    size[0]
    4
    size[4]
    2

    7Someone asks about computer 3. find(3) walks 3 → 2 → 0, and on the way back it points 3 straight at the root. This is path compression: the next find from 3 takes one hop.

  8. parent
    00
    10
    20
    30
    44
    54
    012345
    ra
    0
    rb
    4

    8Cable 5–3. find(5) is 4 (size 2) and find(3) is 0 (size 4). Root 4's group is smaller, so the swap makes ra = 0 and rb = 4: 4 will go under 0, not the other way round.

  9. parent
    00
    10
    20
    30
    40
    54
    012345
    groups
    1
    size[0]
    6

    9Root 4 now reports to 0. Only computer 5 got one hop further from its root. Had we put 0 under 4, the four computers in the bigger group would all have moved one hop further.

  10. parent
    00
    10
    20
    30
    40
    50
    012345
    groups
    1
    union(5, 1)
    false

    10Cable 5–1. find(5) walks 5 → 4 → 0 and compresses 5 onto 0. find(1) is 0 too. Same root, so union returns false: this cable connects nothing new.

  11. parent
    00
    10
    20
    30
    40
    50
    012345
    groups
    1
    longest find
    1 hop

    11Six cables, one group. Every computer now points straight at root 0, so any find is one hop. The tree stayed flat because small groups went under big ones, and finds flattened what was left.

1 / 11

Two tricks that keep trees flat

The whole cost of union-find is the walk inside find. A short walk means fast operations. Without care, the walks get long.

Suppose you always put the first root under the second, and the cables arrive as 0–1, 1–2, 2–3, 3–4. Each union hangs the whole existing group under one new computer. You get a chain, and find(0) walks past every computer. With n computers that is n steps per find, no better than searching the graph.

Careless: a chain 4 3 2 1 0 4 hops By size: flat 0 1 2 3 4 every find: 1 hop
The same five computers after the same four cables. Left: a careless union builds a chain, and find(0) takes 4 hops. Right: union by size keeps everyone 1 hop from the root.

Union by size. Always hang the smaller group's root under the bigger group's root. A computer only gets one hop further from its root when its group joins a group at least as big, so its group at least doubles each time. A group of n can only double about log₂ n times, so no walk is longer than log₂ n hops. For a million computers, that is 20.

Path compression. Every time find walks up a path, point every computer on it straight at the root. You did the walk anyway, so this costs nothing extra, and the next find from any of them takes one hop.

The code

class UnionFind {
  constructor(n) {
    this.parent = Array.from({ length: n }, (_, i) => i); // everyone is their own root
    this.size = new Array(n).fill(1);
    this.groups = n;
  }
 
  find(x) {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]); // path compression
    }
    return this.parent[x];
  }
 
  union(a, b) {
    let rootA = this.find(a);
    let rootB = this.find(b);
    if (rootA === rootB) return false; // already in the same group
    if (this.size[rootA] < this.size[rootB]) [rootA, rootB] = [rootB, rootA];
    this.parent[rootB] = rootA; // smaller tree goes under the bigger one
    this.size[rootA] += this.size[rootB];
    this.groups--;
    return true;
  }
}
class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))  # everyone is their own root
        self.size = [1] * n
        self.groups = n
 
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # path compression
        return self.parent[x]
 
    def union(self, a, b):
        root_a, root_b = self.find(a), self.find(b)
        if root_a == root_b:
            return False  # already in the same group
        if self.size[root_a] < self.size[root_b]:
            root_a, root_b = root_b, root_a
        self.parent[root_b] = root_a  # smaller tree goes under the bigger one
        self.size[root_a] += self.size[root_b]
        self.groups -= 1
        return True

To ask "are a and b connected?", compare find(a) with find(b). The groups counter tells you how many separate pieces the network is in, without any search. It drops by one only when a union really merges two groups.

The return value of union is useful on its own. When it returns false, the two computers were already connected, so the new cable closes a loop. That is how you detect a cycle in an undirected graph while reading its edges one at a time, and it is the heart of Kruskal's algorithm in Minimum Spanning Trees.

ApproachTimeSpaceWhy
Re-run BFS or DFS per questionO(V + E) per questionO(V)Walks the whole network every time.
Union-find, no tricksO(n) per operationO(n)Trees can turn into chains.
Union by size onlyO(log n) per operationO(n)A group doubles each time a node gets deeper, so depth stays under log₂ n.
Union by size + path compressionO(α(n)) amortizedO(n)α is the inverse Ackermann function. It is at most 4 for any input you could ever store, so treat it as constant.

"Amortized" means averaged over a long run of operations. One find can still take a few hops, but it flattens the path it walked, so later finds are cheaper.

Common mistakes

Check yourself

Quick check

parent = [0, 0, 1, 3, 3]. Which computers are in the same group as 2?

Root A has a group of 10 and root B has a group of 3. With union by size, what does union do?

You read the edges of an undirected graph one at a time and call union(u, v) on each. One call returns false. What does that tell you?

What to practise next

Union-find returns in Minimum Spanning Trees, where it decides which cheap cables are worth laying. First, though, graphs get weights: Dijkstra's Algorithm finds the fastest route when every road has a travel time.