Master Disjoint Set Union (Union-Find) completely — what it is, naive implementation, path compression optimization, union by rank, combined O(α(n)) amortized complexity, Kruskal
What Is DSU?
A Disjoint Set Union (DSU), also called Union-Find, is a data structure that efficiently manages a collection of elements partitioned into disjoint (non-overlapping) groups called sets.
It supports two core operations:
- Find(x): Which group does element x belong to? Returns the "representative" (root) of x's set.
- Union(x, y): Merge the groups containing x and y into a single group.
Real-world analogy: Think of students at a school. Initially each student is their own group. When friendships form, groups merge. DSU answers: "Are Alice and Bob in the same friend group?" and "Connect Alice's group with Bob's group."
Step 1 — Naive Implementation
class DSUNaive:
"""
Naive DSU using parent array.
parent[i] = parent of element i
If parent[i] == i, then i is the root of its set.
"""
def __init__(self, n):
self.parent = list(range(n)) # Initially each element is its own root
def find(self, x):
"""Find root of x's set by following parent chain."""
while self.parent[x] != x:
x = self.parent[x]
return x
def union(self, x, y):
"""Merge the sets containing x and y."""
root_x = self.find(x)
root_y = self.find(y)
if root_x != root_y:
self.parent[root_x] = root_y # Attach x's root under y's root
def connected(self, x, y):
"""Return True if x and y are in the same set."""
return self.find(x) == self.find(y)
Problem with naive: Worst case is a chain: 0→1→2→3→...→n. Find takes O(n). Union of n elements creates a path of length n — O(n²) total.
Step 2 — Path Compression
Idea: After finding the root of a set, make every visited node point directly to the root. Future finds on the same path are O(1).
def find_with_compression(self, x):
"""
Path compression: point every node on the path directly to root.
"""
if self.parent[x] != x:
self.parent[x] = self.find_with_compression(self.parent[x]) # Recurse + compress
return self.parent[x]
Visualization:
Before find(4): After find(4) with path compression:
0 0
| / | \
1 1 2 4
|
2
|
4
All nodes now point directly to root 0 → future finds are O(1)
Step 3 — Union by Rank
Idea: Always attach the smaller tree under the root of the larger tree. This keeps trees shallow.
Rank = upper bound on the height of the tree.
class DSUByRank:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n # All trees start with rank 0
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, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry: return False # Already in same set
# Union by rank: attach smaller rank under larger rank
if self.rank[rx] < self.rank[ry]:
self.parent[rx] = ry
elif self.rank[rx] > self.rank[ry]:
self.parent[ry] = rx
else:
self.parent[ry] = rx # Equal rank: attach ry under rx
self.rank[rx] += 1 # Rank of rx increases by 1
return True # Merged successfully
Complete Optimized DSU
class DSU:
"""
DSU with path compression + union by rank.
Both find() and union() run in O(α(n)) amortized — effectively O(1).
α(n) = inverse Ackermann function ≤ 4 for all practical n.
"""
def __init__(self, n: int):
self.parent = list(range(n))
self.rank = [0] * n
self.size = [1] * n # Size of each set (useful for getting set sizes)
self.num_components = n # Track number of distinct components
def find(self, x: int) -> int:
"""Find root with full path compression. O(α(n)) amortized."""
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x: int, y: int) -> bool:
"""
Merge sets of x and y. Returns True if they were different sets.
Uses union by rank.
"""
rx, ry = self.find(x), self.find(y)
if rx == ry:
return False # Already connected
# Attach smaller rank tree under larger rank tree
if self.rank[rx] < self.rank[ry]:
rx, ry = ry, rx # Ensure rx has higher rank
self.parent[ry] = rx # Attach ry under rx
self.size[rx] += self.size[ry]
if self.rank[rx] == self.rank[ry]:
self.rank[rx] += 1
self.num_components -= 1
return True
def connected(self, x: int, y: int) -> bool:
"""Check if x and y are in the same set. O(α(n))."""
return self.find(x) == self.find(y)
def get_size(self, x: int) -> int:
"""Return the size of the set containing x. O(α(n))."""
return self.size[self.find(x)]
Complexity Analysis
| Operation | Naive | Path Compression Only | Union by Rank Only | Both |
|---|
| Find | O(n) | O(log n) amortized | O(log n) | O(α(n)) |
| Union | O(n) | O(log n) | O(log n) | O(α(n)) |
α(n) = inverse Ackermann function. For any input you will ever see in practice, α(n) ≤ 4. It grows so slowly it is effectively constant.
Application 1 — Kruskal's Minimum Spanning Tree
DSU makes Kruskal's algorithm clean and efficient.
def kruskal_mst(n, edges):
"""
Find Minimum Spanning Tree using Kruskal's algorithm.
n = number of vertices
edges = list of (weight, u, v) sorted by weight
Time: O(E log E) for sorting + O(E α(V)) for DSU operations
"""
edges.sort() # Sort by weight
dsu = DSU(n)
mst_edges = []
total_weight = 0
for weight, u, v in edges:
if dsu.union(u, v): # If u and v are in different components
mst_edges.append((u, v, weight))
total_weight += weight
if len(mst_edges) == n - 1:
break # MST is complete when we have n-1 edges
return mst_edges, total_weight
# Example graph:
edges = [
(1, 0, 1), # Edge 0-1 weight 1
(2, 0, 2), # Edge 0-2 weight 2
(3, 1, 2), # Edge 1-2 weight 3
(4, 1, 3), # Edge 1-3 weight 4
(5, 2, 3), # Edge 2-3 weight 5
]
mst, weight = kruskal_mst(4, edges)
print(f"MST edges: {mst}")
print(f"Total weight: {weight}")
# MST edges: [(0,1,1), (0,2,2), (1,3,4)]
# Total weight: 7
Application 2 — Cycle Detection in Undirected Graph
def has_cycle(n, edges):
"""
Detect cycle in undirected graph.
If adding an edge connects two vertices already in the same component → cycle.
Time: O(E α(V))
"""
dsu = DSU(n)
for u, v in edges:
if not dsu.union(u, v): # Already connected
return True # Adding this edge creates a cycle
return False
print(has_cycle(4, [(0,1), (1,2), (2,3)])) # False — no cycle
print(has_cycle(4, [(0,1), (1,2), (2,0)])) # True — cycle 0→1→2→0
print(has_cycle(5, [(0,1),(1,2),(3,4),(2,3),(1,4)])) # True
Application 3 — Number of Connected Components
def count_components(n, edges):
"""Count number of connected components in graph."""
dsu = DSU(n)
for u, v in edges:
dsu.union(u, v)
return dsu.num_components
print(count_components(5, [(0,1),(1,2),(3,4)])) # 2 — {0,1,2} and {3,4}
print(count_components(5, [])) # 5 — no edges
Application 4 — Largest Component Size
def largest_component(n, edges):
dsu = DSU(n)
for u, v in edges:
dsu.union(u, v)
return max(dsu.get_size(i) for i in range(n))
print(largest_component(6, [(0,1),(1,2),(2,3),(4,5)])) # 4 — {0,1,2,3}
Interview Problems
Problem 1 — Friend Circles
def find_circle_num(is_connected):
"""
Number of friend groups. is_connected[i][j]=1 if i and j are friends.
"""
n = len(is_connected)
dsu = DSU(n)
for i in range(n):
for j in range(i+1, n):
if is_connected[i][j]:
dsu.union(i, j)
return dsu.num_components
Problem 2 — Accounts Merge (Merge emails belonging to same person)
def accounts_merge(accounts):
"""
Merge accounts that share at least one email.
Each account: [name, email1, email2, ...]
"""
email_to_id = {} # email → account index
dsu = DSU(len(accounts))
for i, account in enumerate(accounts):
for email in account[1:]:
if email in email_to_id:
dsu.union(i, email_to_id[email])
else:
email_to_id[email] = i
# Group emails by root account
from collections import defaultdict
root_to_emails = defaultdict(set)
for email, acc_id in email_to_id.items():
root = dsu.find(acc_id)
root_to_emails[root].add(email)
result = []
for root, emails in root_to_emails.items():
result.append([accounts[root][0]] + sorted(emails))
return result
Summary
DSU with path compression + union by rank:
| Property | Value |
|---|
| Find | O(α(n)) ≈ O(1) in practice |
| Union | O(α(n)) ≈ O(1) in practice |
| Space | O(n) |
| Key feature | Track which elements are in the same connected component |
Classic uses:
- Kruskal's MST algorithm
- Cycle detection in undirected graphs
- Connected components
- Dynamic connectivity problems (edges are added, not removed)
- Network connectivity, friend circles, image segmentation
*Next: Suffix Array*