Skip to main content

Welcome to a Grasp lesson

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

Lesson 60

Implementing Union-Find

Recommended resources

Hello! Welcome to your next lesson on advanced graph data structures.

In our last lesson, we explored topological sorting for Directed Acyclic Graphs, a crucial pattern for problems involving dependencies and scheduling. As we previewed, we're now shifting our focus from directed graphs to a powerful tool often used with undirected graphs and dynamic connectivity problems: the Union-Find data structure.

Today's goal is to implement the Union-Find data structure with its essential optimizations: path compression and union by rank/size. This structure, also known as Disjoint Set Union (DSU), is a high-ROI pattern for technical interviews. It's the key to efficiently solving problems related to network connectivity, cycle detection in undirected graphs, and grouping related items. By the end of this lesson, you will have a complete, interview-ready implementation template.

1. What is Union-Find?

Imagine you have a collection of items, and you want to group them into a number of disjoint (non-overlapping) sets. The Union-Find data structure is designed to do two things very quickly:

  1. Find: Determine which set a particular item belongs to.
  2. Union: Merge two sets into a single new set.

Its primary application is to solve dynamic connectivity problems. For example, given a set of nodes in a graph, can we efficiently answer queries like "Are node A and node B connected?" while edges are being added to the graph dynamically? A naive DFS/BFS would be too slow if you have many queries. Union-Find provides a near-constant time solution.

To build a strong mental model, let's start with a short video that offers a great analogy and a high-level visual introduction.

Union Find Visually Explained

This video from 'Hello Byte' introduces the core concepts of Union-Find using a company hierarchy analogy. It will help you visualize the structure and its basic operations.

Watch the first 6 minutes and 35 seconds of the video (from 00:00 to 06:35). Focus on understanding the three main operations (make_set, find, union) and how an array can be used to represent the parent-child relationships in the underlying tree structure.

As the video explains, we can represent these sets as a forest of trees, where each tree corresponds to a set. The root of the tree acts as the representative (or identifier) for that set.

  • The find(x) operation involves traversing from node x up to the root of its tree.
  • The union(x, y) operation merges the sets containing x and y by making the root of one tree a child of the root of the other.

2. The Need for Optimizations

A simple implementation of this idea has a critical flaw. If we aren't careful about how we perform the union operation, we can end up creating very tall, skewed trees that resemble linked lists.

Consider uniting elements in a chain: union(0,1), then union(1,2), then union(2,3), and so on. The resulting tree would be a straight line. In this worst-case scenario, a find operation would have to traverse all the way up the chain, making its time complexity . For interview settings where performance is key, this is unacceptable.

To achieve the near-constant time complexity that makes Union-Find so powerful, we need two optimizations.


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

These two techniques work together to keep the trees in our forest extremely flat, ensuring that the find operation remains fast.

Union by Rank / Union by Size

This optimization applies to the union operation. Instead of arbitrarily attaching one tree to another, we use a heuristic to decide which tree goes under which.

  • Union by Rank: We keep track of the "rank" of each root, which is an upper bound on the height of its tree. When uniting two trees, we always attach the root of the shorter tree (lower rank) to the root of the taller tree (higher rank). This prevents the overall tree height from growing unnecessarily. If the ranks are equal, we can pick one, attach the other, and increment the rank of the new root.
  • Union by Size: This is a similar, often more intuitive, alternative. We track the number of nodes (size) in the set represented by each root. When uniting, we attach the root of the smaller set to the root of the larger set.
Comparison of Basic Union vs. Union by Rank in Disjoint Set Union
CAPTION: A comparison showing how a naive `union` (left) can create a skewed tree, while `union by rank` (right) maintains a more balanced, flatter structure by attaching the shorter tree to the taller one.

Path Compression

This optimization applies to the find operation. When we call find(x), we traverse the path from x up to its root. Path compression does a simple, brilliant thing: after finding the root, it makes every node along that path a direct child of the root.

Path Compression in Union-Find
CAPTION: An illustration of path compression. After calling `find(3)`, the path from 3 to the root (0) is flattened. Node 3 is now connected directly to the root 0, making future `find` calls for 3 (or its children) much faster.

With these two optimizations combined, the amortized time complexity of both union and find operations becomes nearly constant, specifically , where is the Inverse Ackermann function. This function grows so slowly that for any practical input size N, it's less than 5.

Let's watch a detailed walkthrough of implementing these optimizations.

G-46. Disjoint Set | Union by Rank | Union by Size | Path Compression

This comprehensive video from 'take U forward' dives deep into the implementation details of both optimizations. It provides a step-by-step trace of how the data structure evolves.

Please watch two segments from this video: Union by Rank & Path Compression (05:13 - 22:07): This part explains the logic of Union by Rank and Path Compression in detail. Pay close attention to how the parent and rank arrays are updated. Union by Size (35:12 - 41:07): This segment introduces Union by Size as an alternative. Notice how it's conceptually similar to Union by Rank but uses a size array instead.


4. Implementation Template

Now, let's consolidate everything into a clean, reusable Python class. This template is what you should aim to reproduce in an interview. You will see it implements the logic discussed in the video.

We will have a class UnionFind that is initialized with the number of elements n. It will maintain a parent array and either a rank or size array.

  • __init__(self, n): Initializes parent[i] = i for all elements, and sets their initial rank to 0 or size to 1.
  • find(self, i): Finds the root of i and performs path compression using recursion.
  • union(self, i, j): Merges the sets containing i and j using union by rank or union by size.

The following resource provides excellent, clean code for both variations.

Union By Rank and Path Compression in Union-Find ...

This GeeksForGeeks article provides complete, optimized implementations of Union-Find in multiple languages. We will focus on the Python code as it serves as a perfect template.

Study the Python code snippets for both 'Union by Rank' and 'Union by Size'. Take note of the recursive implementation of find() for path compression and the logic within the unionSets()/unionBySize() methods. You don't need to read the whole article, just focus on understanding these code blocks.

Here's a summary of the Union by Size implementation, which is a very common interview choice:

class UnionFind:
    def __init__(self, n):
        # Each node is its own parent initially
        self.parent = list(range(n))
        # Each component has a size of 1 initially
        self.size = [1] * n

    def find(self, i):
        # Base case: if i is its own parent, it's the root
        if self.parent[i] == i:
            return i
        
        # Recursive call to find the root, with path compression
        self.parent[i] = self.find(self.parent[i])
        return self.parent[i]

    def union(self, i, j):
        # Find the roots of the sets containing i and j
        root_i = self.find(i)
        root_j = self.find(j)
        
        # If they are already in the same set, do nothing
        if root_i != root_j:
            # Union by size: attach the smaller tree to the root of the larger tree
            if self.size[root_i] < self.size[root_j]:
                root_i, root_j = root_j, root_i # Ensure root_i is the larger set
            
            self.parent[root_j] = root_i
            self.size[root_i] += self.size[root_j]
            return True # Return True to indicate a successful merge
        
        return False # Return False if they were already connected
Test your understanding!

Imagine you have a UnionFind structure for 5 elements (0-4), implemented with Union by Rank and Path Compression. The parent array is [0, 1, 2, 3, 4] and the rank array is [0, 0, 0, 0, 0].

Trace the state of the parent and rank arrays after these operations:

  1. union(0, 1)
  2. union(2, 3)
  3. union(0, 2)

What are the final parent and rank arrays?

Show answer
  1. union(0, 1):

    • find(0) -> 0. find(1) -> 1.
    • Ranks are equal (both 0). Attach 1 to 0.
    • parent becomes [0, 0, 2, 3, 4].
    • rank becomes [1, 0, 0, 0, 0] (rank of root 0 increases).
  2. union(2, 3):

    • find(2) -> 2. find(3) -> 3.
    • Ranks are equal (both 0). Attach 3 to 2.
    • parent becomes [0, 0, 2, 2, 4].
    • rank becomes [1, 0, 1, 0, 0] (rank of root 2 increases).
  3. union(0, 2):

    • find(0) -> 0. find(2) -> 2.
    • rank[0] is 1, rank[2] is 1. Ranks are equal. Attach 2 to 0.
    • parent becomes [0, 0, 0, 2, 4].
    • rank becomes [2, 0, 1, 0, 0] (rank of root 0 increases).

Final State:

  • parent: [0, 0, 0, 2, 4]
  • rank: [2, 0, 1, 0, 0]

Conclusion

You have now built one of the most efficient data structures for handling dynamic connectivity. Its elegance and performance make it a favorite in competitive programming and a valuable tool for your interview arsenal.

Key Takeaways:

  • Union-Find (DSU) is specialized for managing disjoint sets and answering connectivity queries.
  • A naive implementation leads to performance, which is too slow.
  • Union by Rank/Size keeps the underlying trees flat during union operations.
  • Path Compression flattens the tree structure during find operations.
  • Together, these optimizations give Union-Find a nearly constant amortized time complexity ().
  • The space complexity is to store the parent and rank/size arrays.

This table from the "Union-Find" chapter in shawonnotes is a great summary of when to use this data structure.

Problem Type Union-Find? Why?
Connected Components ✅ Yes Efficient for merging nodes dynamically.
Cycle Detection (Undirected) ✅ Yes Detects cycles in O(1) amortized operations.
Cycle Detection (Directed) ❌ No Use DFS or Kahn’s Algorithm instead.
Kruskal’s Algorithm (MST) ✅ Yes Quickly processes edge unions.
Dynamic Path/Connectivity Queries ✅ Yes Answers connectivity in O(1) amortized time.

Preview of the Next Lesson:

Now that you have mastered the implementation of the Union-Find data structure, our next lesson will focus on its practical application. We will apply Union-Find to efficiently solve common graph connectivity problems, such as counting the number of connected components in a graph and detecting redundant connections. You'll see just how powerful this tool is in action.