Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
405: Implementing Union-Find (Disjoint Set)
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
There are no comments for now.