Hello! Let's continue our journey into advanced graph algorithms.
In our last lesson, we built a complete, optimized Union-Find (or Disjoint Set Union) data structure from the ground up. You now have a powerful UnionFind class in your toolkit, complete with path compression and union by rank/size optimizations, making its operations nearly constant time.
Today, we'll put that tool to work. Our goal is to apply the Union-Find data structure to efficiently solve graph connectivity problems, with a specific focus on a classic interview question: counting the number of connected components in a graph. This will demonstrate the practical, high-ROI nature of the Union-Find pattern you've learned.
1. Understanding Connected Components
A connected component is a subgraph in which any two vertices are connected to each other by paths, and which is connected to no additional vertices in the supergraph. In simpler terms, it's a distinct, self-contained "island" within a larger graph.

For a problem like this, your first thought might be to use Depth-First Search (DFS) or Breadth-First Search (BFS). You could iterate through each node, and if a node hasn't been visited, you'd start a traversal (DFS/BFS) from it, increment a component counter, and mark every node in that component as visited. This works and has a time complexity of , where is the number of vertices and is the number of edges.
However, the Union-Find data structure provides a more natural and often more efficient way to model this problem, especially in dynamic scenarios.
2. The Union-Find Strategy for Counting Components
The core idea is to reframe the problem from "traversing to find islands" to "merging sets until no more merges are possible."
Here is the algorithm:
- Initialization: Assume at the very beginning, no nodes are connected. Therefore, each of the
Nnodes is its own isolated component. We start withcount = Ncomponents. - Process Edges: We iterate through each edge
(u, v)in the graph. - Union Operation: For each edge, we try to
unionthe sets containinguandv.- If
uandvare already in the same set (i.e., their roots are the same), this edge is redundant. It connects two nodes that were already connected. The number of components does not change. - If
uandvare in different sets, theunionoperation will merge them. This means we have just connected two previously separate components into one larger component. Therefore, we decrement our componentcountby 1.
- If
- Final Count: After processing all the edges, the final value of
countis our answer.
This approach elegantly tracks the reduction of components as edges are "added" to the graph. Let's watch a video that walks through this exact logic and contrasts it with the DFS approach.
Number of Connected Components in an Undirected Graph - Union Find - Leetcode 323 - Python
The NeetCode video 'Number of Connected Components in an Undirected Graph' is a perfect resource for this topic. It's tailored for interview preparation and clearly explains how Union-Find is a natural fit for this problem.
Watch the entire video. Pay close attention to these key parts: The DFS/BFS approach (00:00 - 04:07): Understand this as the baseline to compare against. The Union-Find logic (04:07 - 10:50): Focus on how the algorithm starts with n components and decrements the count with each successful union. The Python implementation (10:50 - 15:53): Observe how the UnionFind data structure is used to solve the problem.
3. Implementation and Analysis
Let's integrate this logic with the UnionFind class we developed in the previous lesson. A key design choice is to have the union method return a boolean: True if a merge occurred and False otherwise. This makes the calling code clean and explicit.
Here's the complete solution in Python:
# Assuming the UnionFind class from the previous lesson is available
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n
# This is a new attribute for this specific problem
self.num_components = n
def find(self, i):
if self.parent[i] == i:
return 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:
# Union by size
if self.size[root_i] < self.size[root_j]:
root_i, root_j = root_j, root_i
self.parent[root_j] = root_i
self.size[root_i] += self.size[root_j]
# A merge happened, so decrement the component count
self.num_components -= 1
return True # Indicate a successful union
return False # No union was performed
# --- Main function to solve the problem ---
def count_connected_components(n, edges):
"""
Counts the number of connected components in a graph.
Args:
n (int): The number of nodes, labeled 0 to n-1.
edges (list of lists): A list of edges, e.g., [[0, 1], [1, 2], [3, 4]].
Returns:
int: The number of connected components.
"""
if n == 0:
return 0
uf = UnionFind(n)
for u, v in edges:
uf.union(u, v)
return uf.num_components
Notice how clean the main function is. We initialize UnionFind and simply loop through the edges, calling union. The component counting logic is neatly encapsulated within the UnionFind class itself.
An Alternative Counting Method
Another way to find the number of components is to process all unions first and then count the number of unique roots at the end. Each unique root represents one connected component.
# This function uses the same UnionFind class, but a different counting strategy.
def count_connected_components_alternative(n, edges):
if n == 0:
return 0
# We don't need the `num_components` attribute in the UF class for this method
uf = UnionFind(n)
for u, v in edges:
uf.union(u, v)
# After all unions, count the number of unique roots.
# Each node's root is found by calling find().
root_set = {uf.find(i) for i in range(n)}
return len(root_set)
This alternative is conceptually simple but slightly less efficient, as it requires an extra pass over all nodes at the end to count the roots. The first method of decrementing the count during the union process is generally preferred as it gives the final answer immediately after processing the edges.
Test your understanding!
Given n = 6 nodes and the following edges: [[0, 1], [2, 3], [0, 4], [3, 5]]
Trace the number of connected components using the decrementing union method. What is the component count after each edge is processed?
- Initial state
- After
union(0, 1) - After
union(2, 3) - After
union(0, 4) - After
union(3, 5)
Show answer
- Initial state:
num_components = 6. Sets are{0}, {1}, {2}, {3}, {4}, {5}. union(0, 1): Nodes 0 and 1 were in different sets. They are merged.num_componentsbecomes 5. Sets are now{0, 1}, {2}, {3}, {4}, {5}.union(2, 3): Nodes 2 and 3 were in different sets. They are merged.num_componentsbecomes 4. Sets are now{0, 1}, {2, 3}, {4}, {5}.union(0, 4): Nodes 0 and 4 were in different sets. The set{4}is merged into{0, 1}.num_componentsbecomes 3. Sets are now{0, 1, 4}, {2, 3}, {5}.union(3, 5): Nodes 3 and 5 were in different sets. The set{5}is merged into{2, 3}.num_componentsbecomes 2. Sets are now{0, 1, 4}, {2, 3, 5}.
The final number of connected components is 2.
Complexity Analysis
- Time Complexity:
- for initializing the
UnionFinddata structure. - For each of the edges, we perform one
unionoperation, which involves twofindcalls. With optimizations, this loop takes time. - Since is nearly constant, the complexity is effectively linear in the number of nodes and edges.
- for initializing the
- Space Complexity: to store the
parentandsize/rankarrays.
This performance is excellent and makes Union-Find a go-to solution for connectivity-based problems in interviews.
Conclusion
You have now successfully applied the Union-Find data structure to solve a common and practical graph problem. This lesson moved from theory to application, a crucial step in preparing for technical interviews where you're expected not just to know data structures, but to know when and how to use them effectively.
Key Takeaways:
- Counting connected components can be intuitively modeled as a process of merging sets.
- The algorithm starts with
Ncomponents and decrements the count for each successfulunionof two previously disconnected sets. - The Union-Find data structure is the perfect tool for this, providing near-constant time operations for
findandunion. - The overall time complexity of is highly efficient and competitive with DFS/BFS-based solutions.
Preview of the Next Lesson:
We will stay on the topic of undirected graphs but shift our focus to another fundamental problem: detecting cycles. While you might notice that a union operation on two nodes already in the same set implies a cycle, we will explore the canonical DFS-based approach for cycle detection. This will involve tracking visited nodes and their parents during a traversal to identify "back edges." This will add another important graph pattern to your problem-solving repertoire.