Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Graphs

Union-find (disjoint set union)

Lesson · 11 min read · Graphs · Union-find and spanning trees

Keep track of groups as connections arrive, answering "are these two connected?" in nearly constant time.

Groups that only ever merge

Traversals answer connectivity questions about a graph that is already built. Many problems instead add connections one at a time and ask, after each, whether two items are now connected, or how many groups there are, or whether the new connection closed a loop. Re-running a search after every edge costs O(n + m) each time. Union-find answers each question in almost constant time.

It supports two operations on a collection of disjoint groups:

  • find(x): the representative (root) of x's group. Two items are connected exactly when their roots are equal.
  • union(a, b): merge the groups containing a and b.

A forest of parent pointers

Each group is stored as a tree in which every node points to its parent, and the root points to itself. find follows pointers up to the root; union points one root at the other. Two refinements keep the trees flat:

  • Union by size: attach the smaller tree under the larger one, so no tree grows taller than log₂ n.
  • Path compression: after a find, point every node passed on the way directly at the root, so the next lookup is one step.

Together they make each operation run in amortised O(α(n)) time, where α is the inverse Ackermann function: below 5 for any n that fits in the universe.

Union-find

find(x)

  1. If parent[x] ≠ x, set parent[x] = find(parent[x]) (path compression).
  2. Return parent[x].

union(a, b)

  1. Let ra = find(a), rb = find(b). If equal, return false: already connected.
  2. Attach the root of the smaller group under the other, add the sizes, return true.

Step by step

Six nodes, edges arriving in the order 0–1, 2–3, 1–3, 0–2, 4–5, 3–4. The tag on each root is its group size.

Union-find with union by size and path compression. Arrows point from a node to its parent.

  1. Step 1 / 7
    0root·11root·12root·13root·14root·15root·1
    groups6

    Every node starts as its own root: n separate groups. An arrow points from a node to its parent.

  2. Step 2 / 7
    0root·212root·13root·14root·15root·1
    groups5

    Edge 0–1: roots 0 and 1 differ. Attach the smaller tree (root 1) under the larger (root 0).

  3. Step 3 / 7
    0root·212root·234root·15root·1
    groups4

    Edge 2–3: roots 2 and 3 differ. Attach the smaller tree (root 3) under the larger (root 2).

  4. Step 4 / 7
    0root·41234root·15root·1
    groups3

    Edge 1–3: roots 0 and 2 differ. Attach the smaller tree (root 2) under the larger (root 0).

  5. Step 5 / 7
    0root·41234root·15root·1
    groups3

    Edge 0–2: find(0) = find(2) = 0. They are already in the same group, so this edge would close a cycle.

  6. Step 6 / 7
    0root·41234root·25
    groups2

    Edge 4–5: roots 4 and 5 differ. Attach the smaller tree (root 5) under the larger (root 4).

  7. Step 7 / 7
    0root·612345
    groups1

    Edge 3–4: roots 0 and 4 differ. Attach the smaller tree (root 4) under the larger (root 0). Path compression also re-pointed 3 at its root. 1 group remains.

root that absorbed a groupboth ends already connected

The fourth edge, 0–2, is the interesting one: both ends already have root 0, so it adds nothing to connectivity. In a graph it is exactly the edge that closes a cycle.

Implementation

The problem at the end of this module adds edges until one closes a cycle, which is the most direct use of union returning false. In six languages:

def redundant_edge(edges):
    parent = list(range(len(edges) + 1))     # nodes are 1..n
    size = [1] * (len(edges) + 1)

    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]    # path halving
            x = parent[x]
        return x

    for a, b in edges:
        ra, rb = find(a), find(b)
        if ra == rb:
            return [a, b]                    # already connected: this edge closes the cycle
        if size[ra] < size[rb]:
            ra, rb = rb, ra
        parent[rb] = ra                      # smaller tree under the larger
        size[ra] += size[rb]
    return []

The same code is the reference solution to The connection that closes a loop, where it is compiled and run against the tests in all six languages.

TimeO(m · α(n))
SpaceO(n)

Effectively linear. Without the two refinements a chain can form and each find becomes O(n).

Use union-find when edges arrive over time, when you need to count groups as they merge, or inside Kruskal's algorithm. Use a traversal when the graph is fixed and you need paths or distances, which union-find does not provide, or when edges can be deleted, which it cannot handle.