Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 77

Implementing Disjoint Set Union (DSU)

Recommended resources

Hello! Welcome to the next lesson in our graph algorithms module.

In the previous lesson, we saw how to find connected components using standard traversals like DFS and BFS. This approach is perfect when the graph is static. But what if we need to build a graph by adding edges one by one and frequently ask, "Are these two nodes connected yet?" Running a full DFS/BFS for every query would be inefficient.

Today, we'll learn a data structure designed specifically for this "dynamic connectivity" problem. Your goal for this lesson is to implement the Disjoint Set Union (DSU) data structure with path compression and union by rank/size. This is a favorite topic in technical interviews and a powerhouse tool for competitive programming, so mastering it will be a significant step toward your goals.

Disjoint Set Union with Union by Rank and Path Compression
This image visualizes the core optimizations we'll discuss today, Path Compression and Union by Rank. The company logos underscore its importance in the industry.

1. What is a Disjoint Set Union (DSU)?

A Disjoint Set Union (DSU) data structure, also known as Union-Find, manages a collection of disjoint sets. "Disjoint" simply means that no element belongs to more than one set. Think of it like a group of people. Initially, everyone is in their own individual group (a set of size one). As we discover relationships (e.g., friendships), we merge these groups.

DSU is built to do two things very, very fast:

  1. find(element): Determine which set an element belongs to. It does this by returning a single, unique "representative" or "leader" for that set.
  2. union(element1, element2): Merge the two sets containing element1 and element2 into a single set.

How does this help us? To check if two nodes are connected, we simply find the representative for each. If their representatives are the same, they are in the same set, meaning they are connected.

To get a clear overview of these concepts, let's start with a video.

Disjoint Set | Union By Rank and Path Compression

This video from 'take U forward' provides a great introduction to the DSU data structure, explaining its purpose and its two main operations.

Watch the first 5 minutes of the video (00:00 - 04:59). Focus on understanding: The concept of a 'component' and its 'parent' (representative). How the find and union operations conceptually work to answer connectivity questions.


2. Representing Disjoint Sets and the Need for Optimization

The most common way to represent these sets is using trees, where each set is a tree and the root of the tree is the set's representative. We can implement this entire structure using a simple array, let's call it parent.

For a set of n elements (numbered 0 to n-1):

  • parent[i] stores the parent of element i.
  • If parent[i] == i, then i is the root of its tree (the representative of its set).

The find(i) operation works by traversing up from i using the parent array until it reaches the root. The union(i, j) operation first finds the roots of i and j, and then makes one root the parent of the other.

However, a naive union operation (e.g., always making j's root the parent of i's root) can lead to very tall, skinny trees, essentially becoming linked lists. In this worst case, a find operation could take time. To make DSU truly powerful, we need optimizations.

The following resources explain these optimizations beautifully.

1.12 Disjoint Sets Data Structure - Weighted Union and Collapsing Find

This classic video by Abdul Bari explains the DSU structure and its two crucial optimizations: union by weight (similar to size/rank) and collapsing find (path compression). He masterfully illustrates the array representation alongside the graphical one.

Please watch from 11:10 to 25:28. Pay close attention to: Graphical and Array Representation (11:10 - 15:15): How a tree of sets is mapped to a parent array. Union by Weight/Size (15:15 - 23:01): Understand the logic of attaching the smaller tree to the larger one and how the size (stored as a negative number at the root) is updated. Collapsing Find / Path Compression (23:01 - 25:28): Grasp the simple but powerful idea of re-wiring nodes to point directly to the root after a find operation.


3. Key Optimizations: Union by Rank/Size and Path Compression

As you saw in the video, two optimizations dramatically improve DSU's performance:

1. Union by Rank or Size (Weighted Union):
This optimization minimizes the height of the trees. When uniting two sets, instead of arbitrarily connecting them, we attach the smaller tree to the root of the larger tree.

  • Union by Rank: We track the "rank" (an upper bound on the height) of each tree. The tree with the lower rank is attached to the root of the tree with the higher rank. If ranks are equal, one is attached to the other, and the rank of the new root is incremented.
  • Union by Size: We track the number of elements (size) in each tree. The tree with fewer elements is attached to the root of the one with more elements. This is often simpler to implement and just as effective.
DSU Union by Rank Implementation Example
This image shows the process of Union by Rank. Notice how the rank is used to decide which tree becomes a subtree, keeping the overall structure as flat as possible.

2. Path Compression:
This optimization flattens the tree structure whenever we perform a find operation. When we find the root of an element x, we make every node on the path from x to the root point directly to the root. This makes subsequent find operations on any of those nodes extremely fast.

When used together, these optimizations make the DSU operations run in nearly constant time on average. The amortized time complexity is , where is the Inverse Ackermann function. This function grows so slowly that for any practical input size you'll encounter in an interview or competition, it's less than 5. So, for all practical purposes, the complexity is considered constant time.


4. Java Implementation

Now, let's translate these concepts into a Java class. You'll build a UnionFind class that incorporates both Path Compression and Union by Size/Rank.

Introduction to Disjoint Set (Union-Find Data Structure)

The GeeksforGeeks article provides a clear walkthrough and complete Java implementations for DSU with both Union by Rank and Union by Size.

Read through the following sections to understand the full Java implementation: Optimization (Path Compression and Union by Rank/Size): Start by reviewing the explanation of the two main optimizations. Java Implementation with Union by Rank: Study the DisjointUnionSets class. Pay close attention to the find method (which uses path compression) and the union method (which uses the rank array). Union by Size: Read the description of this alternative approach. Java Implementation with Union by Size: Study the UnionFind class that uses unionBySize. This is another equally valid and often preferred implementation.

Let's consolidate the implementation for Union by Size with Path Compression, as it's a very common and effective combination.

public class UnionFind {
    private int[] parent; // parent[i] = parent of element i
    private int[] size;   // size[i] = number of elements in subtree rooted at i
    private int count;    // Number of disjoint sets

    public UnionFind(int n) {
        parent = new int[n];
        size = new int[n];
        count = n; // Initially, n elements in n disjoint sets
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    /**
     * Finds the representative of the set containing element i, with path compression.
     */
    public int find(int i) {
        if (parent[i] == i) {
            return i;
        }
        // Path compression: set parent[i] to the root
        return parent[i] = find(parent[i]); 
    }

    /**
     * Merges the sets containing elements i and j, using union by size.
     */
    public void union(int i, int j) {
        int rootI = find(i);
        int rootJ = find(j);

        if (rootI == rootJ) {
            return; // Already in the same set
        }

        // Union by size: attach smaller tree to the root of larger tree
        if (size[rootI] < size[rootJ]) {
            parent[rootI] = rootJ;
            size[rootJ] += size[rootI];
        } else {
            parent[rootJ] = rootI;
            size[rootI] += size[rootJ];
        }
        count--; // One set has been merged, so decrement the count
    }

    /**
     * Returns true if elements i and j are in the same set.
     */
    public boolean connected(int i, int j) {
        return find(i) == find(j);
    }
    
    /**
     * Returns the number of disjoint sets.
     */
    public int getCount() {
        return count;
    }
}
Test your understanding!

Consider a UnionFind structure with 8 elements (0-7). Trace the state of the parent and size arrays after the following operations are performed in order: union(0, 1), union(2, 3), union(0, 2). Assume the implementation above (union by size).

Show answer
  1. Initial State:

    • parent = [0, 1, 2, 3, 4, 5, 6, 7]
    • size = [1, 1, 1, 1, 1, 1, 1, 1]
  2. After union(0, 1):

    • root(0) is 0, root(1) is 1. Sizes are equal (both 1).
    • Let's say we attach 1 to 0. parent[1] becomes 0. size[0] becomes 2.
    • parent = [0, 0, 2, 3, 4, 5, 6, 7]
    • size = [2, 1, 1, 1, 1, 1, 1, 1]
  3. After union(2, 3):

    • root(2) is 2, root(3) is 3. Sizes are equal (both 1).
    • Let's say we attach 3 to 2. parent[3] becomes 2. size[2] becomes 2.
    • parent = [0, 0, 2, 2, 4, 5, 6, 7]
    • size = [2, 1, 2, 1, 1, 1, 1, 1]
  4. After union(0, 2):

    • root(0) is 0, root(2) is 2. size[0] is 2, size[2] is 2. Sizes are equal.
    • Let's say we attach 2 to 0. parent[2] becomes 0. size[0] becomes 4.
    • Final parent = [0, 0, 0, 2, 4, 5, 6, 7]
    • Final size = [4, 1, 2, 1, 1, 1, 1, 1]

The final set is {0, 1, 2, 3} with root 0.


Conclusion

You have now learned one of the most elegant and efficient data structures in computer science. The Disjoint Set Union is your go-to tool for any problem involving partitioning elements into sets and dynamically checking for connectivity.

Key Takeaways:

  • DSU efficiently handles dynamic connectivity problems with its find and union operations.
  • It's represented as a forest of trees, implemented with a simple parent array.
  • Naive implementations are slow, leading to performance in the worst case.
  • The combination of Path Compression and Union by Rank/Size optimizes operations to have a nearly constant amortized time complexity, .
  • The implementation is concise and powerful, making it a valuable tool for interviews and competitive programming.

Preview of the Next Lesson:
Now that you have this powerful tool, what can you do with it? In the next lesson, we will see a direct and classic application of DSU. We will use it to implement Kruskal's algorithm to find the Minimum Spanning Tree (MST) of a graph. You'll see how DSU's ability to efficiently detect cycles is central to how Kruskal's algorithm works.