AI SWE Prep
0/23in Data Structures & Algorithms
Data Structures & Algorithms

Union-Find (Disjoint Sets)

Merge groups and ask 'same set?' in near-constant time. The quiet workhorse of connectivity and cycle problems.

~13 minLesson 26 of 60
Your progressNot started

Union-Find tracks groups as they merge. It answers two operations very quickly: “Which group is this item in?” and “Merge these two groups.” That is the exact shape of connectivity problems where edges arrive and components collapse over time.

DFS and BFS are excellent when you have the whole graph and want to explore it. Union-Find is better when the work is edge-centric: process connections one by one, merge components, and ask whether two nodes were already connected.

The mental model

Each item points to a parent. A root points to itself and represents the whole set. find(x) follows parent pointers to the root; union(a, b) connects the two roots if they differ.

The two optimizations are what make the structure fast:

  • Path compression — after finding a root, make every node on that path point directly to the root.
  • Union by rank or size — attach the smaller or shallower tree under the larger one so parent chains stay short.
class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False

        if self.size[ra] < self.size[rb]:
            ra, rb = rb, ra
        self.parent[rb] = ra
        self.size[ra] += self.size[rb]
        return True

With both optimizations, operations are effectively constant time in practice: near-O(1) amortized, formally O(alpha(n)) where alpha is the inverse Ackermann function. It grows so slowly that for interview-scale inputs it behaves like a small constant.

Union-Find · disjoint setsStep 1 / 6
0
1
2
3
4
5
parent[ ]
00
11
22
33
44
55
Every element starts as its own singleton set (parent points to itself).
elements in a union opUnion by size keeps trees shallow; path compression flattens them further — near O(1) amortized.

Pattern recognition

Variations

Worked problems

All three problems use the same DSU class. The only difference is how the input is converted into union operations.

LeetCode 323Medium
  • Union-Find
  • Graph

Number of Connected Components in an Undirected Graph

Given n labeled nodes and an edge list for an undirected graph, return how many connected components the graph has.

Approach. Start with n isolated components. For each edge, union its two endpoints. If the union actually merges two different roots, decrement the component count. Edges inside an existing component do not change the count.

Show solution
class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        if self.size[ra] < self.size[rb]:
            ra, rb = rb, ra
        self.parent[rb] = ra
        self.size[ra] += self.size[rb]
        return True


def count_components(n, edges):
    dsu = DSU(n)
    components = n
    for a, b in edges:
        if dsu.union(a, b):
            components -= 1
    return components

Complexity. O((n + e) alpha(n)) time for e edges and O(n) space. The inverse Ackermann factor is effectively constant.

LeetCode 547Medium
  • Union-Find
  • Matrix

Number of Provinces

Given an adjacency matrix where is_connected[i][j] says whether two cities are directly connected, return the number of connected city groups.

Approach. Treat each city as a DSU node. Only scan one triangle of the matrix because the graph is undirected and the matrix is symmetric. Every 1 merges two cities into the same province.

Show solution
class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n

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

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        if self.size[ra] < self.size[rb]:
            ra, rb = rb, ra
        self.parent[rb] = ra
        self.size[ra] += self.size[rb]
        return True


def find_circle_num(is_connected):
    n = len(is_connected)
    dsu = DSU(n)
    provinces = n

    for i in range(n):
        for j in range(i + 1, n):
            if is_connected[i][j] == 1 and dsu.union(i, j):
                provinces -= 1

    return provinces

Complexity. O(n^2 alpha(n)) time because the matrix has O(n^2) entries, and O(n) space for the DSU arrays.

LeetCode 684Medium
  • Union-Find
  • Cycle detection

Redundant Connection

Given edges for an undirected graph that started as a tree and then received one extra edge, return the edge that creates the cycle.

Approach. In a tree, every new edge should connect two previously separate components. Process edges in order and union their endpoints. The first edge whose endpoints already have the same root is redundant because it closes a cycle.

Show solution
class DSU:
    def __init__(self, n):
        self.parent = list(range(n + 1))
        self.rank = [0] * (n + 1)

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        if self.rank[ra] < self.rank[rb]:
            self.parent[ra] = rb
        elif self.rank[ra] > self.rank[rb]:
            self.parent[rb] = ra
        else:
            self.parent[rb] = ra
            self.rank[ra] += 1
        return True


def find_redundant_connection(edges):
    dsu = DSU(len(edges))
    for a, b in edges:
        if not dsu.union(a, b):
            return [a, b]
    return []

Complexity. O(e alpha(n)) time and O(n) space. The failed union is the signal that the new edge connects two nodes already in the same component.

The classic pitfall

Your progressNot started