Union-Find (Disjoint Set Union)
1 / 12
Tracks connected components with two operations:
Nearly O(1) per operation with optimizations
Each element points to parent; root is representative
1 3
/ /
2 4
Find(2) → 1 (root)
Find(4) → 3 (root)
Initially: each element is its own parent
While finding root, make nodes point directly to it
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
Flattens tree → future finds are O(1)
Attach smaller tree under larger tree
if rank[root_x] < rank[root_y]:
parent[root_x] = root_y
elif rank[root_x] > rank[root_y]:
parent[root_y] = root_x
else:
parent[root_y] = root_x
rank[root_x] += 1
Keeps trees shallow
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * 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, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry: return False
# ... rank comparison ...
return True
def count_components(n, edges):
uf = UnionFind(n)
components = n
for a, b in edges:
if uf.union(a, b):
components -= 1
return components
Each successful union merges two components
def find_redundant(edges):
uf = UnionFind(len(edges) + 1)
for a, b in edges:
if not uf.union(a, b):
return [a, b] # Creates cycle!
Union returns False = already connected = cycle
Key: email → first account that had it
| Operation | Naive | Optimized |
|---|---|---|
| Find | O(n) | O(α(n)) ≈ O(1) |
| Union | O(n) | O(α(n)) ≈ O(1) |
α(n) = inverse Ackermann ≈ constant
Union-Find: streaming edges, frequent connectivity queries
BFS/DFS: need paths, single-pass traversal