Master Union-Find (Disjoint Set Union) in graph contexts — finding connected components, dynamic connectivity queries, cycle detection in undirected graphs, Kruskal
Union-Find Recap
Union-Find (DSU) maintains groups of elements and supports:
- Find(x): Which group is x in? Returns the root/representative.
- Union(x, y): Merge the groups containing x and y.
With path compression + union by rank: O(α(n)) ≈ O(1) per operation.
class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.size = [1] * n
self.components = 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
if self.rank[rx] < self.rank[ry]: rx, ry = ry, rx
self.parent[ry] = rx
self.size[rx] += self.size[ry]
if self.rank[rx] == self.rank[ry]: self.rank[rx] += 1
self.components -= 1
return True
def connected(self, x, y):
return self.find(x) == self.find(y)
def component_size(self, x):
return self.size[self.find(x)]
Application 2 — Cycle Detection in Undirected Graph
def has_cycle(n, edges):
"""
Detect cycle in undirected graph.
If two vertices being connected are 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 → adding this edge creates cycle
return True
return False
print(has_cycle(4, [(0,1),(1,2),(2,3)])) # False — path, no cycle
print(has_cycle(4, [(0,1),(1,2),(2,0)])) # True — triangle
print(has_cycle(5, [(0,1),(1,2),(3,4),(2,3),(1,4)])) # True
Application 3 — Redundant Connection
def find_redundant_connection(edges):
"""
Find the last edge that makes an undirected graph contain a cycle.
(LeetCode 684)
Time: O(n α(n))
"""
n = len(edges)
dsu = DSU(n + 1) # Vertices 1-indexed
for u, v in edges:
if not dsu.union(u, v):
return [u, v] # This edge creates a cycle
return []
print(find_redundant_connection([[1,2],[1,3],[2,3]])) # [2,3]
print(find_redundant_connection([[1,2],[2,3],[3,4],[1,4],[1,5]])) # [1,4]
Application 4 — Dynamic Connectivity
class DynamicConnectivity:
"""
Support both edge additions and connectivity queries.
No edge removal (for removal, need more complex structures).
"""
def __init__(self, n):
self.dsu = DSU(n)
def add_edge(self, u, v):
self.dsu.union(u, v)
def are_connected(self, u, v):
return self.dsu.connected(u, v)
def num_components(self):
return self.dsu.components
def component_size(self, v):
return self.dsu.component_size(v)
dc = DynamicConnectivity(6)
dc.add_edge(0, 1)
dc.add_edge(2, 3)
print(dc.are_connected(0, 1)) # True
print(dc.are_connected(0, 2)) # False
print(dc.num_components()) # 4
dc.add_edge(1, 2)
print(dc.are_connected(0, 3)) # True (0-1-2-3)
print(dc.num_components()) # 3
Application 5 — Largest Component
def largest_component(n, edges):
"""Find size of largest connected component."""
dsu = DSU(n)
for u, v in edges:
dsu.union(u, v)
return max(dsu.component_size(i) for i in range(n))
print(largest_component(6, [(0,1),(1,2),(2,3),(4,5)])) # 4 ({0,1,2,3})
Application 6 — Accounts Merge
from collections import defaultdict
def accounts_merge(accounts):
"""
Merge accounts sharing at least one email.
DSU on email indices.
Time: O(n × m × α(n×m)) where m = avg emails per account.
"""
email_to_id = {}
email_to_name = {}
idx = 0
for account in accounts:
name = account[0]
for email in account[1:]:
if email not in email_to_id:
email_to_id[email] = idx
email_to_name[email] = name
idx += 1
dsu = DSU(idx)
for account in accounts:
first_email_id = email_to_id[account[1]]
for email in account[2:]:
dsu.union(first_email_id, email_to_id[email])
# Group emails by root
root_to_emails = defaultdict(set)
for email, eid in email_to_id.items():
root_to_emails[dsu.find(eid)].add(email)
result = []
for root, emails in root_to_emails.items():
any_email = next(iter(emails))
result.append([email_to_name[any_email]] + sorted(emails))
return result
accounts = [
["John","johnsmith@mail.com","john_newyork@mail.com"],
["John","johnsmith@mail.com","john00@mail.com"],
["Mary","mary@mail.com"],
["John","johnnybravo@mail.com"]
]
merged = accounts_merge(accounts)
for acc in merged:
print(acc)
DSU vs BFS/DFS for Connected Components
| Feature | DSU | BFS/DFS |
|---|
| Time per query | O(α(n)) ≈ O(1) | O(V+E) for full traversal |
| Build time | O(E α(V)) | O(V+E) |
| Dynamic edges | ✅ — just call union | ❌ — must rebuild |
| Find path | ❌ — only connectivity | ✅ — can trace path |
| Detect cycles | ✅ | ✅ |
| Space | O(V) | O(V) |
Use DSU when: Only need connectivity (not path), edges are added dynamically. Use BFS/DFS when: Need actual paths, graph is static, need ordering.
Summary
DSU solves graph connectivity problems efficiently:
- Count components: Initialize n components, decrement on each union
- Cycle detection: Edge (u,v) creates cycle iff
find(u) == find(v) - Dynamic edges: Add edge with union, query connectivity with find
- Kruskal's MST: Core data structure for cycle-free edge addition
All operations O(α(n)) — essentially constant for all practical inputs.
*Next Lesson: Graph Interview Problems*