Union Find
Table of Contents
- # 0) Concept
- # 0-0) Key Optimizations
- # 0-1) Types
- # 0-2) Algorithm Pattern / Template
- # 0-3) Pattern-Specific Code Examples
- # 1) Example Problems with Code References
- # 2) Diagrams
- # Basic Union Operations
- # Path Compression Visualization
- # Union by Rank Example
- # Path Compression in Action
- # 3) Tips & Pitfalls
# Union Find
- Efficiently determines connectivity between nodes in dynamic graphs
- When to use it: Dynamic connectivity queries, cycle detection, MST algorithms, grouping elements
- Key LeetCode problems: Graph Valid Tree, Number of Islands, Accounts Merge, Friend Circles
- Data structures: Parent array, size/rank array for optimization
- States: Connected components, parent-child relationships
Time Complexity: Nearly O(1) per operation with optimizations
# 0) Concept
# 0-0) Key Optimizations
Union Find achieves nearly O(1) performance through two critical optimizations:
Path Compression: Applied in find() operation
- Makes each visited node point directly to the root
- Flattens the tree structure during traversal
- Recursive:
parent[x] = find(parent[x]) - Amortizes future lookups to O(1)
Union by Rank/Size: Applied in union() operation
- Always attach smaller tree to larger treeβs root
- Keeps tree height balanced (logarithmic)
- Prevents degenerate linear chains
- Can track either tree height (rank) or size (count)
Without these optimizations, operations degrade to O(n). With both, time complexity becomes O(Ξ±(n)) where Ξ± is the inverse Ackermann function (effectively constant).
# 0-1) Types
- Basic Connectivity: Check if nodes are connected, count components
- Cycle Detection: Determine if adding edge creates cycle
- Dynamic MST: Kruskalβs algorithm for minimum spanning trees
- Weighted Union Find: Handle ratios/weights between nodes (LC 399)
- Grid Problems: 2D grid connectivity (Number of Islands variants)
# 0-2) Algorithm Pattern / Template
Core Operations:
find(x): Get root parent of x with path compressionunion(x, y): Connect two nodes, return false if already connectedconnected(x, y): Check if two nodes are in same component
Template (Union by Size):
class UnionFind {
int[] parent, size;
int components;
public UnionFind(int n) {
parent = new int[n];
size = new int[n];
components = n;
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
// Path Compression: flatten tree by making nodes point directly to root
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // Compress path during recursion
}
return parent[x];
}
// Union by Size: attach smaller tree to larger tree
public boolean union(int x, int y) {
int rootX = find(x), rootY = find(y);
if (rootX == rootY) return false; // Already connected
// Always attach smaller size to larger size
if (size[rootX] < size[rootY]) {
parent[rootX] = rootY;
size[rootY] += size[rootX];
} else {
parent[rootY] = rootX;
size[rootX] += size[rootY];
}
components--;
return true;
}
}
Alternative Template (Union by Rank):
class UnionFind {
int[] parent, rank;
int components;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
components = n;
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0; // Initial rank is 0
}
}
// Path Compression: recursively flatten tree structure
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // Make x point directly to root
}
return parent[x];
}
// Union by Rank: attach lower rank tree to higher rank tree
public void union(int x, int y) {
int rootX = find(x), rootY = find(y);
// Already in the same component
if (rootX == rootY) return;
// Attach smaller rank tree to larger rank tree
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY; // X's tree becomes child of Y
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX; // Y's tree becomes child of X
} else {
// Equal ranks: attach either way, increment rank of new root
parent[rootX] = rootY;
rank[rootY]++;
}
components--;
}
}
Key Differences: Size vs Rank
- Union by Size: Tracks actual count of nodes in each tree
- Useful when you need component sizes
- Updates size after every union
- Union by Rank: Tracks approximate tree height (upper bound)
- More space efficient (rank grows slowly)
- Rank only increases when merging equal-rank trees
- With path compression, rank != actual height
Edge Cases:
- Single node graphs
- Already connected nodes
- Invalid indices
# 0-3) Pattern-Specific Code Examples
# Pattern 1: Basic Connectivity - Cycle Detection
Problem: LC 261 - Graph Valid Tree
public boolean validTree(int n, int[][] edges) {
// A tree must have exactly n-1 edges
if (edges.length != n - 1) return false;
UnionFind uf = new UnionFind(n);
for (int[] edge : edges) {
if (!uf.union(edge[0], edge[1])) {
return false; // Cycle detected
}
}
return true;
}
# Pattern 2: Component Counting
Problem: LC 323 - Number of Connected Components
public int countComponents(int n, int[][] edges) {
UnionFind uf = new UnionFind(n);
int components = n;
for (int[] edge : edges) {
if (uf.union(edge[0], edge[1])) {
components--;
}
}
return components;
}
# Pattern 3: Find Redundant Edge (Cycle Detection)
Problem: LC 684 - Redundant Connection
public int[] findRedundantConnection(int[][] edges) {
UnionFind uf = new UnionFind(edges.length + 1);
for (int[] edge : edges) {
int x = edge[0], y = edge[1];
if (!uf.union(x, y)) {
return edge; // This edge creates a cycle
}
}
return null;
}
# Pattern 4: 2D Grid Connectivity
Problem: LC 200 - Number of Islands
public int numIslands(char[][] grid) {
int rows = grid.length, cols = grid[0].length;
UnionFind uf = new UnionFind(rows * cols);
int islands = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == '1') {
islands++;
int idx = r * cols + c;
// Check 4 directions
int[][] dirs = {{0,1}, {1,0}, {0,-1}, {-1,0}};
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
&& grid[nr][nc] == '1') {
int nidx = nr * cols + nc;
if (uf.union(idx, nidx)) {
islands--;
}
}
}
}
}
}
return islands;
}
# Pattern 5: Weighted Union Find (with Ratios)
Problem: LC 399 - Evaluate Division
class WeightedUnionFind {
Map<String, String> parent;
Map<String, Double> ratio; // ratio[x] = x / parent[x]
public WeightedUnionFind() {
parent = new HashMap<>();
ratio = new HashMap<>();
}
public String find(String x) {
if (!parent.containsKey(x)) {
parent.put(x, x);
ratio.put(x, 1.0);
}
if (!x.equals(parent.get(x))) {
String originalParent = parent.get(x);
parent.put(x, find(originalParent));
ratio.put(x, ratio.get(x) * ratio.get(originalParent));
}
return parent.get(x);
}
public void union(String x, String y, double value) {
String rootX = find(x);
String rootY = find(y);
if (!rootX.equals(rootY)) {
parent.put(rootX, rootY);
ratio.put(rootX, value * ratio.get(y) / ratio.get(x));
}
}
public double query(String x, String y) {
if (!parent.containsKey(x) || !parent.containsKey(y)) {
return -1.0;
}
String rootX = find(x);
String rootY = find(y);
if (!rootX.equals(rootY)) return -1.0;
return ratio.get(x) / ratio.get(y);
}
}
public double[] calcEquation(List<List<String>> equations,
double[] values,
List<List<String>> queries) {
WeightedUnionFind uf = new WeightedUnionFind();
for (int i = 0; i < equations.size(); i++) {
String a = equations.get(i).get(0);
String b = equations.get(i).get(1);
uf.union(a, b, values[i]);
}
double[] results = new double[queries.size()];
for (int i = 0; i < queries.size(); i++) {
String c = queries.get(i).get(0);
String d = queries.get(i).get(1);
results[i] = uf.query(c, d);
}
return results;
}
# 1) Example Problems with Code References
# Basic Connectivity & Component Counting
-
LC 200 β Number of Islands: Count connected components in 2D grid
- Java:
leetcode_java/src/main/java/LeetCodeJava/DFS/NumberOfIslands.java:493 - Pattern: Grid to 1D conversion (
row * cols + col), Union Find with 4-directional checks
- Java:
-
LC 261 β Graph Valid Tree: Check if n-1 edges form exactly one component
- Java:
leetcode_java/src/main/java/LeetCodeJava/BFS/GraphValidTree.java:36 - Pattern: Cycle detection, exactly n-1 edges validation
- Java:
-
LC 323 β Number of Connected Components: Basic component counting
- Java:
leetcode_java/src/main/java/LeetCodeJava/Graph/NumberOfConnectedComponentsUndirectedGraph.java:49 - Pattern: Track component count, decrement on successful union
- Java:
# Cycle Detection & Redundancy
- LC 684 β Redundant Connection: Find edge that creates cycle in tree
- Java:
leetcode_java/src/main/java/LeetCodeJava/Tree/RedundantConnection.java:50 - Pattern: Return first edge where
union()fails (cycle detected)
- Java:
# Weighted Union Find
- LC 399 β Evaluate Division: Weighted UF with ratios for equation solving
- Java:
leetcode_java/src/main/java/LeetCodeJava/DFS/EvaluateDivision.java:421 - Pattern: Store ratios, path compression with ratio multiplication
- Java:
# Advanced Applications
- LC 130 β Surrounded Regions: Use dummy node for boundary connected regions
- LC 547 β Friend Circles: Find groups in friendship matrix
- LC 721 β Accounts Merge: Group accounts by shared emails
- LC 886 β Possible Bipartition: Detect bipartite graph conflicts
- LC 1135 β Connecting Cities: MST with Kruskalβs algorithm
- LC 1319 β Network Connections: Minimum operations to connect all nodes
# 2) Diagrams
# Basic Union Operations
Initial: [0] [1] [2] [3] [4]
After union(0,1): [0,1] [2] [3] [4]
1
/
0
After union(2,3): [0,1] [2,3] [4]
1 3
/ /
0 2
# Path Compression Visualization
Before find(1): After find(1):
4 (root) 4 (root)
| /|\
3 1 2 3
|
2
|
1
Call find(1):
- 1 β 2 β 3 β 4 (traversal)
- During return, compress: parent[1] = 4, parent[2] = 4, parent[3] = 4
- Result: All nodes point directly to root
# Union by Rank Example
Initial state:
0 1 2 3
rank: 0 0 0 0
union(0, 1): union(2, 3):
0 2
/ /
1 3
rank[0] = 1 rank[2] = 1
union(0, 2):
0 (rank=2)
/ \
1 2
/
3
Why rank increased:
- rank[0] = 1, rank[2] = 1 (equal)
- Attach 2 to 0, increment rank[0] to 2
# Path Compression in Action
Scenario: find(A) in chain AβBβCβDβE (root)
Step 1: Recursive calls
find(A) calls find(B)
find(B) calls find(C)
find(C) calls find(D)
find(D) calls find(E)
find(E) returns E
Step 2: Path compression during return
parent[D] = E
parent[C] = E β Compression happens here
parent[B] = E β Skip intermediate nodes
parent[A] = E β Direct link to root
Result:
Before: A β B β C β D β E
After: A β E
B β E
C β E
D β E
# 3) Tips & Pitfalls
Common Mistakes:
-
Forgetting Path Compression: Results in O(n) time instead of nearly O(1)
// WRONG: No path compression public int find(int x) { while (parent[x] != x) x = parent[x]; return x; } // CORRECT: With path compression public int find(int x) { if (parent[x] != x) { parent[x] = find(parent[x]); // Flatten on return } return parent[x]; } -
Not Tracking Component Count: Missing decrement in union operation
// WRONG: Forgot to decrement public void union(int x, int y) { parent[find(x)] = find(y); } // CORRECT: Track components public void union(int x, int y) { int rootX = find(x), rootY = find(y); if (rootX != rootY) { parent[rootX] = rootY; components--; // Important! } } -
Index Confusion: Mixing 0-based and 1-based indexing
-
Cycle Detection Timing: Checking after union instead of before
-
Wrong Parent Update: Updating node instead of root in union
// WRONG: Update x directly parent[x] = parent[y]; // CORRECT: Update roots int rootX = find(x), rootY = find(y); parent[rootX] = rootY; -
Confusing Rank with Size:
- Rank = approximate tree height (only increases when merging equal ranks)
- Size = actual node count (always increases by merged size)
How to Optimize:
- Always use path compression in find operation
- Union by size/rank to keep trees balanced
- Track component count for quick queries
- Use iterative find to avoid recursion overhead
Space vs Time Tradeoffs:
- Basic UF: O(n) space, O(n) time per operation
- With optimizations: O(n) space, O(Ξ±(n)) β O(1) time per operation
- Ξ±(n) is inverse Ackermann function, effectively constant for practical inputs
Key Patterns:
- Cycle Detection:
if (find(x) == find(y)) return false; // cycle - Component Counting: Track and decrement count on successful unions
- 2D Grid to 1D: Use
row * cols + colfor coordinate conversion - Dummy Nodes: Connect boundary elements to virtual node for easier processing
- Weighted Relationships: Store ratios/distances for equation-like problems
When NOT to use Union Find:
- Static graphs where DFS/BFS suffice
- Need shortest paths (use Dijkstra/Floyd-Warshall)
- Directed graph strongly connected components (use Tarjanβs)
- Small graphs where simple adjacency checks work