Skip to content
advancedPhase 5 · Graphs

Union Find

Master disjoint set union for connectivity and cycle detection.

1h
5 problems
Topic Progress0%

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:

  1. Find: Determine which set an element belongs to
  2. 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

0/3solved
Redundant Connection
Union-Find Cycle Detection

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
Number of Provinces
Union-Find

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
Accounts Merge
Union-Find on Emails

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?

Question 1 options

2. How does Union-Find detect a cycle in an undirected graph?

Question 2 options

3. What is the primary purpose of Union-Find (Disjoint Set Union)?

Question 3 options

4. What is a common mistake when implementing Union-Find (Disjoint Set Union)?

Question 4 options

Flashcards

Question

What are the two key optimizations in Union-Find?

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?

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)?

Answer

Union-Find (Disjoint Set Union) is a key concept in software engineering.

Question

When to use Union-Find (Disjoint Set Union)?

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

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

  1. Cycle Detection: union() returns false → cycle exists
  2. Connected Components: count after all unions
  3. Dynamic Connectivity: check connected() as edges added
  4. 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