Union-Find Data Structure
What is Union-Find?
Union-Find (also called Disjoint Set Union or DSU) is a data structure that tracks elements partitioned into disjoint (non-overlapping) sets. It supports two operations:
- Find: Determine which set an element belongs to
- Union: Merge two sets into one
Key Concepts
Initial State: After Union(0,1): After Union(2,3): After Union(1,3):
{0} {1} {2} {3} {0,1} {2} {3} {0,1} {2,3} {0,1,2,3}
Represented as tree:
0 1 2 3 0 2 0
| | | | | | |
- - - - 1 3 1
/ \n 2 3
Basic Implementation
public class UnionFind {
private int[] parent;
private int[] rank;
private int components;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
components = n;
// Each element is its own parent initially
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// Find with path compression
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // Path compression
}
return parent[x];
}
// Union by rank
public boolean union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return false; // Already in same set (cycle detected!)
}
// Attach smaller tree under larger tree
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
components--;
return true;
}
// Check if two elements are in same set
public boolean connected(int x, int y) {
return find(x) == find(y);
}
// Get number of connected components
public int getComponents() {
return components;
}
}
Why Path Compression and Union by Rank?
Without optimizations:
- Tree can become tall and skewed
- Find operation: O(n) worst case
With optimizations:
- Tree stays nearly flat
- Find operation: O(α(n)) ≈ O(1) amortized
- α(n) is the inverse Ackermann function (practically constant)
Time Complexity
| Operation | Without Optimization | With Optimization |
|---|---|---|
| Find | O(n) | O(α(n)) ≈ O(1) |
| Union | O(n) | O(α(n)) ≈ O(1) |
| Connected | O(n) | O(α(n)) ≈ O(1) |
Visual Example of Path Compression
Before find(3): After find(3):
0 0
| /|\
1 1 2 3
|
2
|
3
All nodes now point directly to root!
Union-Find Applications
Common Applications
1. Dynamic Connectivity
Determine if two elements are connected as edges are added.
// Example: Number of connected components
public int countComponents(int n, int[][] edges) {
UnionFind uf = new UnionFind(n);
for (int[] edge : edges) {
uf.union(edge[0], edge[1]);
}
return uf.getComponents();
}
2. Cycle Detection in Undirected Graph
public boolean hasCycle(int n, int[][] edges) {
UnionFind uf = new UnionFind(n);
for (int[] edge : edges) {
if (!uf.union(edge[0], edge[1])) {
return true; // Cycle detected!
}
}
return false;
}
3. Redundant Connection
// Find edge that creates cycle
public int[] findRedundantConnection(int[][] edges) {
UnionFind uf = new UnionFind(edges.length + 1);
for (int[] edge : edges) {
if (!uf.union(edge[0], edge[1])) {
return edge; // This edge creates cycle
}
}
return new int[0];
}
4. Accounts Merge
public List<List<String>> accountsMerge(List<List<String>> accounts) {
Map<String, Integer> emailToId = new HashMap<>();
Map<String, String> emailToName = new HashMap<>();
int id = 0;
UnionFind uf = new UnionFind(accounts.size() * 10);
for (List<String> account : accounts) {
String name = account.get(0);
for (int i = 1; i < account.size(); i++) {
String email = account.get(i);
emailToName.put(email, name);
if (!emailToId.containsKey(email)) {
emailToId.put(email, id++);
}
uf.union(emailToId.get(account.get(1)), emailToId.get(email));
}
}
Map<Integer, List<String>> merged = new HashMap<>();
for (String email : emailToId.keySet()) {
int root = uf.find(emailToId.get(email));
merged.computeIfAbsent(root, k -> new ArrayList<>()).add(email);
}
List<List<String>> result = new ArrayList<>();
for (List<String> emails : merged.values()) {
Collections.sort(emails);
List<String> account = new ArrayList<>();
account.add(emailToName.get(emails.get(0)));
account.addAll(emails);
result.add(account);
}
return result;
}
5. Surrounded Regions (with Union-Find)
public void solve(char[][] board) {
int rows = board.length, cols = board[0].length;
UnionFind uf = new UnionFind(rows * cols + 1);
int dummy = rows * cols;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (board[r][c] == 'O') {
int cell = r * cols + c;
// Connect boundary O's to dummy
if (r == 0 || r == rows-1 || c == 0 || c == cols-1) {
uf.union(cell, dummy);
}
// Connect to adjacent O's
if (r > 0 && board[r-1][c] == 'O') uf.union(cell, (r-1)*cols + c);
if (c > 0 && board[r][c-1] == 'O') uf.union(cell, r*cols + (c-1));
}
}
}
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (board[r][c] == 'O' && !uf.connected(r * cols + c, dummy)) {
board[r][c] = 'X';
}
}
}
}
Problem Pattern Recognition
| Problem | Union-Find Usage |
|---|---|
| Connected Components | Count components after unions |
| Cycle Detection | If union returns false, cycle exists |
| Redundant Connection | Find edge that creates cycle |
| Accounts Merge | Group emails by account |
| Similar String Groups | Group equivalent strings |
| Number of Islands II | Dynamic island counting |
Practice Problems
Given a tree with n nodes labeled 1 to n, one extra edge is added. Find the edge that can be removed so the remaining graph is a tree of n nodes.
Example:
Input: edges = [[1,2],[1,3],[2,3]]
Output: 23
Removing edge [2,3] makes the graph a tree: 1-2, 1-3
Solution
```java
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
int[] parent = new int[n + 1];
int[] rank = new int[n + 1];
for (int i = 1; i <= n; i++) parent[i] = i;
for (int[] edge : edges) {
int root1 = find(parent, edge[0]);
int root2 = find(parent, edge[1]);
if (root1 == root2) return edge;
if (rank[root1] < rank[root2]) parent[root1] = root2;
else if (rank[root1] > rank[root2]) parent[root2] = root1;
else { parent[root2] = root1; rank[root1]++; }
}
return new int[0];
}
private int find(int[] parent, int x) {
if (parent[x] != x) parent[x] = find(parent, parent[x]);
return parent[x];
}
```Edge Cases:
- Cycle at beginning
- Cycle at end
- Self-loops
Given n cities and an n x n matrix isConnected where isConnected[i][j] = 1 if city i and j are directly connected, return the number of provinces.
Example:
Input: isConnected = [[1,1,0],[1,1,0],[0,0,1]]
Output: 2
Cities 0 and 1 form one province, city 2 is another.
Solution
```java
public int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
int[] parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (isConnected[i][j] == 1) union(parent, i, j);
Set<Integer> provinces = new HashSet<>();
for (int i = 0; i < n; i++) provinces.add(find(parent, i));
return provinces.size();
}
private int find(int[] parent, int x) {
if (parent[x] != x) parent[x] = find(parent, parent[x]);
return parent[x];
}
private void union(int[] parent, int x, int y) {
int rx = find(parent, x), ry = find(parent, y);
if (rx != ry) parent[rx] = ry;
}
```Edge Cases:
- No connections
- All connected
- Single city
Given a list of accounts where each element is a list of strings with name followed by emails, merge accounts that share at least one email.
Example:
Input: accounts = [["John","john@m.com","john2@m.com"],["John","john@m.com","john@m.com"],["Mary","mary@m.com"]]
Output: [["John","john@m.com","john2@m.com"],["Mary","mary@m.com"]]
Two Johns share an email, so merge.
Solution
```java
public List<List<String>> accountsMerge(List<List<String>> accounts) {
Map<String, String> emailToName = new HashMap<>();
Map<String, String> parent = new HashMap<>();
for (List<String> acc : accounts) {
for (int i = 1; i < acc.size(); i++) {
emailToName.put(acc.get(i), acc.get(0));
parent.putIfAbsent(acc.get(i), acc.get(i));
}
for (int i = 2; i < acc.size(); i++)
union(parent, acc.get(1), acc.get(i));
}
Map<String, Set<String>> groups = new HashMap<>();
for (String email : parent.keySet()) {
String root = find(parent, email);
groups.computeIfAbsent(root, k -> new HashSet<>()).add(email);
}
List<List<String>> result = new ArrayList<>();
for (Map.Entry<String, Set<String>> e : groups.entrySet()) {
List<String> merged = new ArrayList<>(e.getValue());
Collections.sort(merged);
merged.add(0, emailToName.get(e.getKey()));
result.add(merged);
}
return result;
}
private String find(Map<String, String> parent, String x) {
if (!parent.get(x).equals(x)) parent.put(x, find(parent, parent.get(x)));
return parent.get(x);
}
private void union(Map<String, String> parent, String x, String y) {
String rx = find(parent, x), ry = find(parent, y);
if (!rx.equals(ry)) parent.put(rx, ry);
}
```Edge Cases:
- No overlapping emails
- All accounts merge
- Single account
Quiz
1. What is the amortized time complexity of Union-Find operations with path compression and union by rank?
2. How does Union-Find detect a cycle in an undirected graph?
3. What is the primary purpose of Union-Find (Disjoint Set Union)?
4. What is a common mistake when implementing Union-Find (Disjoint Set Union)?
Flashcards
Question
What are the two key optimizations in Union-Find?
Click to reveal answer
Answer
1. Path Compression: During find(), make every node on the path point directly to the root. 2. Union by Rank: Attach smaller tree under larger tree to keep trees flat.
Question
When should you use Union-Find vs DFS/BFS for connected components?
Click to reveal answer
Answer
Union-Find is ideal for dynamic connectivity (edges added over time) and when you need to check connectivity frequently. DFS/BFS is better for static graphs or when you need the actual components.
Question
What is Union-Find (Disjoint Set Union)?
Click to reveal answer
Answer
Union-Find (Disjoint Set Union) is a key concept in software engineering.
Question
When to use Union-Find (Disjoint Set Union)?
Click to reveal answer
Answer
Use Union-Find (Disjoint Set Union) when building production systems that require reliability, scalability, and maintainability.
Question
Union-Find (Disjoint Set Union) best practices
Click to reveal answer
Answer
Follow SOLID principles, write clean code, test thoroughly, document decisions, and monitor in production.
Revision Notes
Key Takeaways
- 1.Union-Find supports efficient set union and membership queries
- 2.Path compression makes find() nearly O(1) amortized
- 3.Union by rank keeps trees balanced
- 4.Cycle detection: if union(x,y) returns false, adding edge creates cycle
- 5.Great for dynamic connectivity problems
Interview Tips
- •Union-Find is perfect for problems asking about connected components
- •Use it when edges are added incrementally
- •For cycle detection in undirected graphs, Union-Find is cleaner than DFS
- •Remember to implement BOTH path compression AND union by rank
- •Practice: Redundant Connection, Accounts Merge, Number of Islands II
Cheat Sheet
Union-Find Cheat Sheet
Core Operations
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]); // Path compression
return parent[x];
}
boolean union(int x, int y) {
int rootX = find(x), rootY = find(y);
if (rootX == rootY) return false; // Same set = cycle!
if (rank[rootX] < rank[rootY]) parent[rootX] = rootY;
else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX;
else { parent[rootY] = rootX; rank[rootX]++; }
return true;
}
Time Complexity
- Without optimization: O(n) per operation
- With path compression + union by rank: O(α(n)) ≈ O(1)
Key Applications
- Cycle Detection: union() returns false → cycle exists
- Connected Components: count after all unions
- Dynamic Connectivity: check connected() as edges added
- Accounts Merge: union emails, group by root
Implementation Checklist
- Initialize parent[i] = i
- Initialize rank[i] = 0
- Path compression in find()
- Union by rank in union()
- Return false from union() if same root
Common Pitfalls
- Forgetting path compression → O(n) find
- Not using union by rank → unbalanced trees
- Off-by-one errors (0-indexed vs 1-indexed)
- Not handling disconnected components