cv.cm

Coding · Data structures

Union-find (disjoint set union) explained: how it works, code and complexity

5 min read

Short answer

Union-find, also called disjoint set union (DSU), keeps track of elements split into non-overlapping groups. It supports two operations: find(x) returns a representative of x's group, and union(x, y) merges the groups of x and y. Each group is stored as a tree of parent pointers. With path compression and union by rank or size, any sequence of operations runs in nearly constant amortized time per operation.

What problem union-find solves

Imagine a network of computers where cables are added one at a time, and after each cable you must answer "are computer A and computer B connected?". Running a graph search for every question costs O(n + m) each time. Union-find answers each question in nearly O(1) by maintaining the connected groups as cables are added.

The same need appears in Kruskal's minimum spanning tree algorithm (do these two endpoints already belong to the same tree?), in detecting cycles in an undirected graph, in grouping accounts that share an email address, in percolation simulations, and in image processing to label connected regions.

The idea in plain words

Every element points to a parent. An element that points to itself is a root, and the root is the representative of the whole group. To find the group of x, follow parent pointers until you reach a root. To merge two groups, find both roots and make one root point to the other.

Two optimizations keep the trees flat:

  • Union by rank (or by size): attach the shorter (or smaller) tree under the taller (or larger) one, so the tree height grows only when two equal trees merge.
  • Path compression: during find, make every node on the path point directly to the root, so the next lookup is one step.

Worked example: five elements

Start with elements 0 to 4, each in its own group. The parent array is [0, 1, 2, 3, 4]. We use union by size and make the second root the child when sizes are equal.

OperationRoots foundParent array afterGroups
start-[0, 1, 2, 3, 4]{0} {1} {2} {3} {4}
union(0, 1)0 and 1, equal size[0, 0, 2, 3, 4]{0, 1} {2} {3} {4}
union(2, 3)2 and 3, equal size[0, 0, 2, 2, 4]{0, 1} {2, 3} {4}
union(1, 3)0 and 2, equal size[0, 0, 0, 2, 4]{0, 1, 2, 3} {4}
find(3)3 to 2 to 0, root 0[0, 0, 0, 0, 4]path compressed: 3 now points to 0
connected(1, 4)roots 0 and 4unchangedFalse, different groups

Python implementation

This version uses union by size and an iterative find with path compression, so it has no recursion depth problems. union returns False when the two elements were already in the same group, which is useful for cycle detection.

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n
        self.groups = n

    def find(self, x):
        root = x
        while self.parent[root] != root:
            root = self.parent[root]
        while self.parent[x] != root:       # path compression
            self.parent[x], x = root, self.parent[x]
        return root

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False                    # already connected
        if self.size[ra] < self.size[rb]:
            ra, rb = rb, ra                 # ra is the larger root
        self.parent[rb] = ra
        self.size[ra] += self.size[rb]
        self.groups -= 1
        return True

    def connected(self, a, b):
        return self.find(a) == self.find(b)

uf = UnionFind(5)
uf.union(0, 1)
uf.union(2, 3)
uf.union(1, 3)
print(uf.find(3), uf.parent)       # 0 [0, 0, 0, 0, 4]
print(uf.connected(1, 4), uf.groups)  # False 2

Example: detecting a cycle in an undirected graph

Process the edges one by one. If both endpoints of an edge already have the same root, the edge closes a cycle. Kruskal's algorithm uses exactly this check after sorting edges by weight.

def has_cycle(n, edges):
    uf = UnionFind(n)
    for a, b in edges:
        if not uf.union(a, b):
            return True
    return False

print(has_cycle(4, [(0, 1), (1, 2), (2, 3)]))  # False
print(has_cycle(4, [(0, 1), (1, 2), (2, 0)]))  # True

Time and space complexity

Versionfind / union (worst case per operation)m operations on n elements
No optimizationsO(n)O(m n), trees can become chains
Union by rank or size onlyO(log n)O(m log n)
Path compression onlyO(log n) amortizedO(m log n) amortized
Both optimizationsO(α(n)) amortizedO(m α(n))

α(n) is the inverse Ackermann function, which grows so slowly that it is at most 4 for any n that could fit in a real computer. In practice this means constant time per operation. Robert Tarjan proved this bound in 1975. Space is O(n) for the parent and size arrays.

Variants and common mistakes

  • Comparing elements instead of roots: union must link find(a) and find(b), not a and b themselves.
  • Assuming parent[x] is the representative. After unions, only find(x) is correct; call find on every element before grouping by parent.
  • Recursive find on large inputs without union by rank can hit Python's recursion limit; the iterative version above avoids it.
  • Elements that are not integers 0 to n-1: map them to indexes with a dict first, or store parents in a dict and add elements lazily.
  • Union-find cannot split groups. If edges are deleted, you need a different structure or an offline approach that processes deletions in reverse.
  • Weighted union-find stores an offset to the parent on each node, which answers questions such as "how much heavier is a than b?".

When to use it and typical problems

Use union-find when groups only ever merge and you need to ask whether two elements are in the same group, or how many groups exist. For a single connectivity question on a static graph, a plain BFS or DFS is simpler. When you need actual paths, use graph search instead, because union-find only knows membership.

Typical problems: number of connected components, number of islands (with grid cells as elements), redundant connection (the edge that creates a cycle), accounts merge, satisfiability of equality equations such as a == b, b != c, and Kruskal's minimum spanning tree.

FAQ

What is the difference between union-find and DSU?

They are two names for the same data structure. Disjoint set union (DSU) and disjoint-set forest are also common names.

Why is union-find almost O(1)?

With union by rank and path compression, the amortized cost per operation is O(α(n)), where α is the inverse Ackermann function. It is no more than 4 for any practical input size.

Do I need both union by rank and path compression?

Either one alone gives O(log n) amortized behavior, which is usually fast enough. Using both gives the near-constant bound and costs only a few extra lines.

Can union-find tell me the members of a group?

Not directly. You can call find on every element and group them by root in O(n α(n)), or maintain a member list per root and merge the lists during union.

Union by rank or union by size?

Both give the same asymptotic bounds. Union by size is often preferred because the size of each group is useful information in itself.

All coding tutorials