Skip to Content
Course content

405: Implementing Union-Find (Disjoint Set)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time during technical interviews: a candidate is asked to track connected components—say, which users are in the same social circle—and their first instinct is to create a list of lists. They think, "I'll just keep a list of sets, and when two people connect, I'll merge their sets."

Merging Lists is a Performance Trap

It sounds intuitive, but let's look at why that fails in a real-world software system. If you have 100,000 users and you're merging lists using list.extend() or set unions, you're potentially moving thousands of elements every time a single connection is made. In the worst case, your "union" operation becomes $O(N)$, turning a simple connectivity check into a performance nightmare that will time out on any decent dataset.

# The "Naive" approach (Don't do this!)
groups = [[0], [1], [2], [3]] 
# To union group 0 and 1, you have to find which list 0 is in, 
# find which list 1 is in, and merge them. 
# That's a lot of searching and shifting memory.

Representing Sets as Trees

The "aha!" moment comes when you stop thinking about sets as containers and start thinking about them as trees. In a Disjoint Set Union (DSU), every element starts as its own parent. If element A and B are connected, we don't move them into a new list; we just make A the parent of B. Now, B "points" to A. If C then connects to B, C points to B, who points to A. A is the "root" or the representative of the entire group.

To find out if two elements are in the same set, you don't check if they are in the same list; you just climb the tree of parent pointers until you hit the root. If they share the same root, they're connected.

class UnionFind:
    def __init__(self, size):
        # Initially, every node is its own parent
        self.parent = list(range(size))

    def find(self, i):
        # Climb the tree until we find the root
        if self.parent[i] == i:
            return i
        return self.find(self.parent[i])

    def union(self, i, j):
        root_i = self.find(i)
        root_j = self.find(j)
        if root_i != root_j:
            # Make one root the parent of the other
            self.parent[root_i] = root_j

Path Compression: The Magic One-Liner

You might notice a problem: if we just keep linking roots, we could end up with a very long, skinny tree (essentially a linked list). This would make our find operation $O(N)$ again. I'll let you in on the trick that makes Union-Find legendary: Path Compression.

During the find operation, once we discover the root, we update every node we visited along the way to point directly to that root. We essentially flatten the tree every time we query it. It’s a tiny change to the code, but it drops the time complexity to nearly constant time—technically the inverse Ackermann function, but for all practical purposes, it's $O(1)$.

def find(self, i):
    if self.parent[i] == i:
        return i
    # This assignment is the "compression"
    self.parent[i] = self.find(self.parent[i])
    return self.parent[i]

Union by Rank: Preventing the Skinny Tree

To be truly rigorous, we shouldn't just pick a random root to be the parent in the union method. We want to attach the shorter tree to the root of the taller tree. We track this using a rank array (which is basically the height of the tree). If we always attach the smaller tree to the larger one, the tree depth stays minimal even before path compression kicks in.

class UnionFind:
    def __init__(self, size):
        self.parent = list(range(size))
        self.rank = [0] * size

    def find(self, i):
        if self.parent[i] != i:
            self.parent[i] = self.find(self.parent[i])
        return self.parent[i]

    def union(self, i, j):
        root_i = self.find(i)
        root_j = self.find(j)
        if root_i != root_j:
            if self.rank[root_i] < self.rank[root_j]:
                self.parent[root_i] = root_j
            elif self.rank[root_i] > self.rank[root_j]:
                self.parent[root_j] = root_i
            else:
                self.parent[root_i] = root_j
                self.rank[root_j] += 1
            return True # Union performed
        return False # Already in the same set



📋 Practical Task

Detecting Redundant Connections in a Network

You are tasked with monitoring a network of servers. Each server is identified by an integer from 0 to $N-1$. You are given a list of connections (edges) between servers. A connection is considered "redundant" if it connects two servers that are already connected through some other path (which would create a cycle in the network).

Your Task: Implement a function find_redundant_connection(n, connections) that returns the first connection in the list that is redundant. If no redundant connections exist, return None.

def find_redundant_connection(n, connections):
    # TODO: Implement the UnionFind class and the logic to detect the cycle
    pass

# Test Cases:
# Case 1: Redundant connection exists
# Servers: 3, Connections: [[0,1], [1,2], [0,2]]
# Expected Output: [0,2] (since 0 and 2 were already connected via 1)

# Case 2: No redundant connection
# Servers: 4, Connections: [[0,1], [1,2], [2,3]]
# Expected Output: None
Rating
0 0

There are no comments for now.

to be the first to leave a comment.