🔗 Union-Find Made Easy

Master Union-Find step-by-step with visual explanations and real-world analogies

🔗 Data Structure âąī¸ 20 min read 🌐 Graph Theory ⚡ Union Operations

🤔 Why Should You Care About Union-Find?

đŸ‘Ĩ

Social Networks

Facebook uses Union-Find to determine friend groups and suggest mutual connections. When you add a friend, their social circles get "unioned" with yours!

🌐

Network Connectivity

Internet routers use Union-Find to quickly determine if two computers can communicate and find alternative paths when connections fail.

đŸ–ŧī¸

Image Processing

Photo editing software uses Union-Find to identify connected regions for features like "magic wand" selection and flood fill.

đŸ—ī¸

Kruskal's Algorithm

Finding minimum spanning trees for network design, circuit layouts, and infrastructure planning relies heavily on Union-Find.

🎮

Game Development

Games use Union-Find for island generation, maze connectivity, and determining reachable areas in strategy games.

📊

Data Clustering

Machine learning algorithms use Union-Find to group similar data points and identify clusters in large datasets.

🌟 Level 1: The Basics (Start Here!)

Level 1

What is Union-Find? Think of it as Social Groups! đŸ‘Ĩ

Imagine managing friend groups at a party. Union-Find helps you quickly answer: "Are Alice and Bob in the same friend group?" and "Let's merge the chess club with the math club!"

đŸ‘Ĩ Building Friend Groups

Let's say we have people: Alice(0), Bob(1), Charlie(2), Diana(3)

Step 1: Everyone starts alone A Alice B Bob C Charlie D Diana Step 2: Union(Alice, Bob) A Root B C D Step 3: After more unions... A Everyone's Root! B C D

Result: All friends are now connected! 🎉

The Two Core Operations 🔧

🔍 Find Operation

Find which group (set) an element belongs to by following parent pointers to the root.

Question: "What group is Bob in?"

Answer: "Alice's group!"

🔗 Union Operation

Merge two groups by connecting their root elements.

Action: "Connect Bob's group with Charlie's group"

Result: "Now they're all friends!"

Your First Union-Find Implementation 🔗
# Simple Union-Find (Disjoint Set Union) class UnionFind: def __init__(self, n): # Everyone starts as their own parent (own group) self.parent = list(range(n)) # [0, 1, 2, 3...] self.rank = [0] * n # Track tree height def find(self, x): """Find the root of element x""" if self.parent[x] != x: # Path compression: point directly to root! self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): """Connect two elements by merging their groups""" root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return # Already connected! # Union by rank: attach smaller tree to larger tree if self.rank[root_x] < self.rank[root_y]: self.parent[root_x] = root_y elif self.rank[root_x] > self.rank[root_y]: self.parent[root_y] = root_x else: self.parent[root_y] = root_x self.rank[root_x] += 1 def connected(self, x, y): """Check if two elements are in the same group""" return self.find(x) == self.find(y) # Try it out! uf = UnionFind(4) # 4 people: 0, 1, 2, 3 uf.union(0, 1) # Alice and Bob become friends uf.union(2, 3) # Charlie and Diana become friends print(uf.connected(0, 1)) # True ✓ print(uf.connected(0, 2)) # False ✗ uf.union(1, 2) # Connect the groups! print(uf.connected(0, 3)) # True ✓ (all connected now!)

âš ī¸ Common Mistake #1: Forgetting Path Compression

# ❌ WRONG - No path compression = slow find! def find_slow(self, x): while self.parent[x] != x: x = self.parent[x] # Long chains = slow! return x

✅ The Right Way: Path Compression

# ✅ CORRECT - Path compression flattens the tree def find_fast(self, x): if self.parent[x] != x: # Make everyone point directly to root! self.parent[x] = self.find(self.parent[x]) return self.parent[x]

🎮 Try It Yourself: Union-Find Simulator

Watch how nodes connect to form groups! Click nodes to explore or use controls below.

🔗 Union Operation
🔍 Find Root
❓ Check Connection

💡 How it works: Each node starts as its own group. Union connects groups, Find identifies the group leader!

🎨 Level 2: Common Union-Find Patterns

Level 2

Pattern 1: Connected Components 🌐

Count how many separate groups exist in a network!

🌐 The Connected Components Algorithm

1
Initialize: Every node starts as its own component
2
Process edges: Union connected nodes
3
Count roots: Each unique root = one component
Connected Components Solution
def count_components(n, edges): """Count connected components in undirected graph""" uf = UnionFind(n) # Connect all edges for u, v in edges: uf.union(u, v) # Count unique roots (components) roots = set() for i in range(n): roots.add(uf.find(i)) return len(roots) # Example: Islands in a grid edges = [(0,1), (1,2), (4,5)] # 6 nodes total result = count_components(6, edges) print(f"Components: {result}") # Output: 3 components # Component 1: {0,1,2} Component 2: {4,5} Component 3: {3}

Pattern 2: Kruskal's Minimum Spanning Tree đŸŒŗ

Find the cheapest way to connect all nodes in a network!

Kruskal's Algorithm with Union-Find
def kruskal_mst(n, edges): """Find Minimum Spanning Tree using Kruskal's algorithm""" # Sort edges by weight (cheapest first) edges.sort(key=lambda x: x[2]) # (u, v, weight) uf = UnionFind(n) mst_edges = [] total_cost = 0 for u, v, weight in edges: # Only add edge if it connects different components if not uf.connected(u, v): uf.union(u, v) mst_edges.append((u, v, weight)) total_cost += weight # Stop when we have n-1 edges (complete tree) if len(mst_edges) == n - 1: break return mst_edges, total_cost # Example: Connect cities with minimum cable cost edges = [ (0, 1, 4), (0, 2, 2), (1, 2, 1), (1, 3, 5), (2, 3, 3) ] mst, cost = kruskal_mst(4, edges) print(f"MST edges: {mst}") print(f"Total cost: {cost}") # Minimum cost to connect all cities

Pattern 3: Redundant Connection 🔄

Find the edge that creates a cycle in the graph!

Detect Redundant Connection
def find_redundant_connection(edges): """Find the edge that creates a cycle""" uf = UnionFind(len(edges) + 1) # +1 for 1-indexed nodes for u, v in edges: # If already connected, this edge creates a cycle! if uf.connected(u, v): return [u, v] # This is the redundant edge uf.union(u, v) return [] # Example: Network with one extra cable edges = [[1,2], [1,3], [2,3]] redundant = find_redundant_connection(edges) print(f"Redundant edge: {redundant}") # [2,3] creates the cycle

đŸ’Ē Level 3: Practice Problems

Practice Time!

Problem Set: From Easy to Hard

E

Easy: Number of Islands đŸī¸

Count islands in a 2D grid where '1' is land and '0' is water. Connected land forms one island.

💡 Hint

Convert 2D coordinates to 1D index. Union adjacent '1' cells.

M

Medium: Accounts Merge 👤

Merge accounts that share email addresses. Each account has a name and list of emails.

💡 Hint

Use email as the element in Union-Find. Map emails to account indices.

H

Hard: Satisfiability of Equality Equations 🧮

Given equations like "a==b" and "b!=c", determine if all equations can be satisfied.

💡 Hint

Process "==" equations first to build groups, then check "!=" equations.

đŸŽ¯ Challenge: Islands Counter

Implement island counting using Union-Find approach.

def num_islands(grid): """Count islands using Union-Find""" # Your code here! # Tip: Convert (row, col) to single index: row * cols + col pass
Show Solution
def num_islands(grid): if not grid or not grid[0]: return 0 rows, cols = len(grid), len(grid[0]) uf = UnionFind(rows * cols) # Union adjacent land cells for i in range(rows): for j in range(cols): if grid[i][j] == '1': # Check right and down neighbors if j + 1 < cols and grid[i][j + 1] == '1': uf.union(i * cols + j, i * cols + j + 1) if i + 1 < rows and grid[i + 1][j] == '1': uf.union(i * cols + j, (i + 1) * cols + j) # Count unique roots for land cells islands = set() for i in range(rows): for j in range(cols): if grid[i][j] == '1': islands.add(uf.find(i * cols + j)) return len(islands)

🚀 Level 4: Advanced Optimizations

Level 4

Path Compression Visualization đŸ›¤ī¸

See how path compression flattens tree structures for faster lookups!

đŸ›¤ī¸ Before vs After Path Compression

Before: Deep Chain

5 4 3 O(n) to find!
→

After: Flat Tree

3 Root 4 5 O(1) to find!
Union-Find with Size Tracking
class WeightedUnionFind: def __init__(self, n): self.parent = list(range(n)) self.size = [1] * n # Track component sizes self.components = n # Total number of components def find(self, x): if self.parent[x] != x: # Path compression with halving self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): root_x, root_y = self.find(x), self.find(y) if root_x == root_y: return False # Already connected # Union by size: smaller component joins larger if self.size[root_x] < self.size[root_y]: root_x, root_y = root_y, root_x self.parent[root_y] = root_x self.size[root_x] += self.size[root_y] self.components -= 1 return True def get_component_size(self, x): """Get size of component containing x""" return self.size[self.find(x)] def get_components_count(self): """Get total number of components""" return self.components

Complexity Analysis 📊

âąī¸ Time Complexity

  • 🔍 Find: O(Îą(n)) - nearly constant!
  • 🔗 Union: O(Îą(n)) - nearly constant!
  • 📊 Îą(n): Inverse Ackermann function
  • ✨ Practical: Effectively O(1) for all inputs

💾 Space Complexity

  • 📊 Parent Array: O(n)
  • 📊 Rank/Size Array: O(n)
  • 📊 Total Space: O(n)
  • ⚡ Very efficient! Linear space

đŸŽ¯ When Îą(n) is Nearly Zero

The inverse Ackermann function Îą(n) grows incredibly slowly:

  • Îą(16) = 3
  • Îą(65536) = 4
  • Îą(2^65536) = 5

For any practical input size, α(n) ≤ 4, making Union-Find operations effectively constant time!

📖 Quick Reference Guide

đŸŽ¯ When to Use Union-Find

  • ✅ Need to group elements into disjoint sets
  • ✅ Frequently check if two elements are connected
  • ✅ Dynamically merge groups/components
  • ✅ Minimum spanning tree algorithms
  • ✅ Detect cycles in undirected graphs
  • ✅ Connected components problems

🚀 Union-Find Operations

  • 🔍 find(x): Get root of element x
  • 🔗 union(x, y): Merge sets containing x and y
  • ❓ connected(x, y): Check if x and y in same set
  • 📊 count(): Number of disjoint sets
  • 📏 size(x): Size of set containing x

⚡ Key Optimizations

  • đŸ›¤ī¸ Path Compression: Point directly to root
  • âš–ī¸ Union by Rank: Attach shorter tree to taller
  • 📊 Union by Size: Attach smaller set to larger
  • đŸŽ¯ Result: Nearly O(1) operations!

🎨 Common Union-Find Patterns

🌐 Connected Components

Count separate groups in graph

đŸī¸ Islands Problem

Connect adjacent land cells

đŸŒŗ MST (Kruskal's)

Avoid cycles in spanning tree

🔄 Cycle Detection

Find redundant edges

đŸ‘Ĩ Account Merge

Group by shared properties

🧮 Equation Satisfiability

Check logical consistency

âš ī¸ Common Pitfalls to Avoid

  • Forgetting path compression: Makes find() slow O(n)
  • No union by rank/size: Creates unbalanced trees
  • Wrong parent initialization: Use parent[i] = i
  • Not handling 0-indexing: Be consistent with indices
  • Inefficient component counting: Track count during union