Begin Algo
Graph Representation · 02 / 02

Union-FindUnion-find

Path compression, union by rank.

Used for: Connectivity checks, clustering, the core of Kruskal

Time complexityO(α(n))
Space complexityO(n)
DifficultyIntermediate
PrerequisitesArrays, recursion, amortised analysis

01Why it exists

Is the network still connected?

Links between data centres are being added and rerouted constantly, and after every change you have to answer "can A still reach B?". Running a fresh BFS for each question is far too expensive.

Why this fitsUnion-find puts the nodes of one connected component into one group, so adding a link is merging two groups and a connectivity query is checking whether two nodes share a group. Both operations are all but constant time once amortised.

Clustering faces in a photo library

Tens of thousands of faces, with an edge drawn between any two that are similar enough. In the end you want to know how many distinct people there are and which person each face belongs to.

Why this fitsEach "similar" edge triggers one merge, and at the end every root stands for one person. Counting connected components is the most direct use of union-find, and Number of Provinces is exactly this problem.

The heart of Kruskal's minimum spanning tree

You add edges in increasing order of weight, but an edge must never close a cycle. How do you decide quickly whether a given edge would create one?

Why this fitsIf both endpoints are already in the same group, the edge is redundant — and union returning false is precisely that signal. Without it, Kruskal would have to re-traverse the graph for every edge it considers.

Reach for it when you see:Whether two things are in the same group, edges added dynamically, how many connected components there are, whether adding this edge closes a cycle, merging only and never splitting.

02The core idea

Union-find maintains a collection of disjoint sets and supports just two operations: find(x) returns the representative of the set containing x, and union(a, b) merges two sets. It represents a forest with a single parent array: each set is a tree, the root is its representative, and parent[root] = root. find walks up the parent links to the root; union hangs the root of one tree underneath the root of another.

In the naive version a tree can grow into a chain, making find O(n). Two optimisations flatten it to near-constant time. Path compression: on the way back out of find, point every node along the path directly at the root, so the next query takes a single step. Union by size (or by rank): always hang the smaller tree under the larger one, which caps the height at log n. Together they give O(m · α(n)) for m operations, where α is the inverse Ackermann function — never above 5 for any n you will ever meet, so treat it as constant.

Its limitation is worth remembering: it merges, it never splits. Problems that require removing an edge ("is it still connected once this link is cut?") are usually turned around: start with none of the edges present and add them back from the last step in reverse. It also only answers whether two nodes are connected, never how to get from one to the other — for a path you still need BFS or DFS.

One feature that often goes unnoticed: union returning false means the two ends were already in the same group, which is to say that this edge closes a cycle. Cycle detection in an undirected graph, edge selection in Kruskal's algorithm, and checking whether a set of edges forms a tree all rely on that signal.

03The algorithm

  1. 1Initialise parent[i] = i, size[i] = 1, and the group count count = n.
  2. 2find(x): while parent[x] ≠ x, recurse to find the root of parent[x] and point parent[x] straight at that root (path compression).
  3. 3union(a, b): find both roots. If they are equal, return false; otherwise hang the smaller root under the larger one, update size, and do count −= 1.
  4. 4connected(a, b) is just find(a) == find(b), and the number of connected components is count.
  5. 5When the nodes are not integers, map them to 0..n−1 with a hash table first. When edges have to be removed, consider processing the queries offline in reverse.

04Interactive demo

Eight nodes with a sequence of union and find operations. Watch how the parent array changes, how the trees grow, and which nodes get re-linked directly to the root when path compression fires.

Start8 nodes · path compression + union by size
01234567
The parent array
i
01234567
parent[i]
01234567
Step 0/32Every node starts as its own group, with parent[i] = i — that is 8 trees consisting of just a root.

05Code

A complete implementation with path compression and union by size, plus its two most common uses: counting connected components and detecting a cycle in an undirected graph. The C++ version uses iterative path halving to avoid recursion.

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))     # everyone starts out as their own root
        self.size = [1] * n              # size of each tree, used when merging
        self.count = n                   # how many groups there are right now

    def find(self, x):
        """Find the root. Path compression: on the way back, hang every node on the path straight off the root"""
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, a, b):
        """Merge two groups. False means they were already one group (that signal detects cycles)"""
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        if self.size[ra] < self.size[rb]:    # union by size: hang the smaller tree under the larger
            ra, rb = rb, ra
        self.parent[rb] = ra
        self.size[ra] += self.size[rb]
        self.count -= 1
        return True

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


# Usage: counting connected components (the heart of LeetCode 547 Number of Provinces)
uf = UnionFind(8)
for a, b in [(0, 1), (2, 3), (1, 3), (4, 5), (6, 7), (5, 7)]:
    uf.union(a, b)
print(uf.count)                 # 2 groups: {0,1,2,3} and {4,5,6,7}
print(uf.connected(0, 4))       # False

# Cycle detection on an undirected graph: if both ends are already one group, this edge closes a cycle
def has_cycle(n, edges):
    uf = UnionFind(n)
    return any(not uf.union(a, b) for a, b in edges)

06Practice

  • LeetCode 547Number of ProvincesMedium
  • LeetCode 684Redundant Connection (cycle detection)Medium
  • LeetCode 200Number of Islands (worth redoing with union-find)Medium
  • LeetCode 721Accounts Merge (the nodes are strings)Medium
  • LeetCode 1584Min Cost to Connect All Points (the setup for Kruskal)Medium
  • LeetCode 1319Number of Operations to Make Network ConnectedMedium