The Quest Begins (The "Why")
I still remember the first time I stared at a LeetCode problem that asked me to count the number of islands in a grid. My initial instinct? Run a BFS/DFS from every unvisited land cell, mark everything reachable, and repeat. It worked, but each query felt like I was re‑exploring the same territory over and over again—like walking the same hallway in a dungeon every time I wanted to open a new door.
Then a friend tossed me another problem: “Given a list of friendships, tell me if two people are in the same social circle.” Again, the naive solution was to rebuild the whole graph for every query. I felt like I was stuck in a grind‑fest, repeating the same low‑level work while the real challenge—understanding the structure of the connections—remain.
That frustration sparked a question: Is there a way to remember what we’ve already discovered about connectivity, so future queries are instant? The answer, as many of you have guessed, lives in a humble but mighty data structure called Union‑Find (also known as Disjoint Set Union, DSU).
The Revelation (The Insight)
At its core, Union‑Find is about two simple ideas:
- Each element starts in its own set – think of every person as a lone adventurer.
- When we learn that two elements belong together, we merge their sets – we call that a union.
The magic isn’t just in merging; it’s in how we find the representative (or “root”) of a set later on. If we naïvely walked up a chain of parents every time, we could end up with O(n) per find—still a grind.
Two optimizations turn this into near‑constant time:
- Union by rank (or size) – always attach the smaller tree under the root of the larger one. This keeps the overall tree shallow, guaranteeing that the height never exceeds log n.
- Path compression – during a find operation, we make every node we pass point directly to the root. It’s like handing every traveler a map that instantly shows the shortest route to the campfire, so next time they don’t need to trek through intermediate nodes.
Together, these tricks give us an amortized O(α(n)) time per operation, where α is the inverse Ackermann function—so slow‑growing it’s practically a constant for any realistic input size. In plain English: after a few unions and finds, the structure flattens itself, and subsequent queries are blazingly fast.
Why does this work? Because we’re not storing the entire connection matrix; we’re storing only enough information to answer “are these two in the same component?” The union operation records the merge, and path compression ensures that the recorded information stays useful without ever becoming a tangled mess.
Wielding the Power (Code & Examples)
Let’s see the theory in action. Below is a compact Union‑Find class in Python, complete with both optimizations.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n)) # each node is its own parent
self.rank = [0] * n # approximate depth of each tree
def find(self, x):
# Find the root with path compression
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # recursion does the compression
return self.parent[x]
def union(self, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry: # already in the same set
return False # nothing to do
# Union by rank: attach smaller rank tree under larger rank tree
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
self.rank[rx] += 1
return True
Why this beats the naive approach
Before (the struggle*:
If we answered “are u and v connected?” by running a BFS/DFS each time, the cost per query is O(V + E). For dense graphs or many queries, that quickly balloons to O(n²).
the victory:
With Union‑Find, each union or find is practically O(1). Building the structure from m edges costs O(m α(n)) ≈ O(m). Answering q connectivity queries after that is O(q α(n)). In interview land, that’s the difference between a solution that times out and one that passes with flying colors.
Real‑world interview problems
1. Number of Islands (LeetCode 200)
Given a 2‑D grid of '1' (land) and '0' (water), count distinct islands.
Naive: BFS/DFS from each land cell → O(m n) time, O(m n) extra space for visited.
Union‑Find solution: Treat each cell as a node. Whenever we see a land cell, union it with its north and west neighbours (if they’re also land). At the end, the number of distinct roots among land cells equals the number of islands.
def numIslands(grid):
if not grid: return 0
rows, cols = len(grid), len(grid[0])
uf = UnionFind(rows * cols)
land = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
land += 1
idx = r * cols + c
# check north
if r > 0 and grid[r-1][c] == '1':
uf.union(idx, (r-1)*cols + c)
# check west
if c > 0 and grid[r][c-1] == '1':
uf.union(idx, r*cols + (c-1))
# count unique roots among land cells
roots = set()
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
roots.add(uf.find(r*cols + c))
return len(roots)
Why it shines: We only touch each cell a constant number of times (checking two neighbours). The DSU does the heavy lifting of merging components in almost‑constant time.
2. Friend Circles (LeetCode 547) – “Are they in the same social network?”
Given an N × N matrix M where M[i][j] = 1 means person i and j are direct friends, return the number of friend circles (connected components).
def findCircleNum(M):
n = len(M)
uf = UnionFind(n)
for i in range(n):
for j in range(i+1, n): # only upper triangle needed
if M[i][j] == 1:
uf.union(i, j)
# count distinct roots
return len({uf.find(i) for i in range(n)})
Again, we avoid rebuilding adjacency lists for each query; the DSU captures the transitive closure of friendship in a single pass.
Common pitfalls (the “traps”)
-
Forgetting path compression – If you implement
findas a simple loop without updating parents, you lose the amortized speed‑up and can degrade to O(n) per find on a long chain. - Union by rank misuse – Attaching the larger tree under the smaller one (or ignoring rank altogether) can create tall trees, again hurting performance. Keep the rule: always attach the lower‑rank tree under the higher‑rank one.
-
Off‑by‑one indexing – When mapping 2‑D coordinates to a 1‑D DSU index, double‑check the formula (
r * cols + c). A slip here will silently union wrong nodes and give you a wrong answer.
Why This New Power Matters
Mastering Union‑Find is like acquiring a quick‑travel spell in an RPG: once you’ve learned the shortcuts, you can zip across the map instead of trudging step‑by‑step. It transforms problems that look like they need heavy graph traversal into near‑linear, almost‑trivial solutions.
Beyond interviews, DSU shows up in real‑world systems:
- Network connectivity – dynamically checking if two routers remain linked after link failures.
- Image processing – labeling connected components in binary images (think Photoshop’s magic wand).
- Percolation theory – modeling how fluids spread through porous media.
Understanding the why behind path compression and union by rank gives you the intuition to tweak the structure for variations (e.g., supporting rollbacks with a persistent DSU, or handling weighted unions for minimum spanning trees).
Your Turn
Grab a problem that feels like a grind—maybe “Accounts Merge” (LeetCode 721) or “Redundant Connection” (LeetCode 684)—and try solving it with Union‑Find. Feel the satisfaction when the code runs in a flash where a naïve BFS would have choked.
And hey, if you ever find yourself stuck in a loop of repetitive work, remember: there’s probably a DSU‑style shortcut waiting to be discovered. Happy coding, and may your finds always be compressed! 🚀
Top comments (0)