Graph - DSA: Number of Provinces - Disjoint Set
1. Problem Statement & Objectives
The Number of Provinces problem asks us to find the total number of connected components in an undirected graph given with vertices.
- Definition of a Province: Two vertices and belong to a single province if there is a path from to (or to ) by any means.
- Input Format: The graph is represented as an Adjacency Matrix of size , where a value of
1atmatrix[i][j]signifies an edge between vertex and vertex , and0indicates no direct edge. - Goal: While this can be solved using traditional traversal techniques like Breadth-First Search (BFS) or Depth-First Search (DFS), our primary objective here is to solve it using the Disjoint Set (DS) data structure. Mastering DS on simpler problems builds the foundational intuition required for solving advanced graph algorithms.
2. Core Mental Model & Real-Life Analogy
Real-Life Analogy: Corporate Organization and Bosses
Imagine a large corporation with multiple departments. Within each department, employees form a communication network, and every department ultimately reports to a single head or Boss (the ultimate parent).
- If you check any employee's ultimate reporting chain, you will eventually trace it up to that department's boss.
- Employees who are themselves bosses represent the distinct departments of the company.
- By simply counting how many people act as their own boss (i.e., root nodes where
ultimate_parent(x) == x), we can immediately determine the exact number of independent departments (provinces) in the corporation.
3. Disjoint Set Recap & Component Merging
Before writing the solution, recall that a Disjoint Set data structure (implemented via Union by Rank or Union by Size with Path Compression) groups elements into disjoint trees.
When we process connections between vertices:
- If vertex
1connects to2, and2connects to3, they form a single tree where1(or another node depending on rank/size) is the root/boss. - If vertex
4connects to5, they form a second tree with4as the boss. - If vertex
6connects to7, they form a third tree with6as the boss.
CODE
Tree 1: Tree 2: Tree 3:
1 4 6
/ \ | |
2 3 5 7
(Boss: 1) (Boss: 4) (Boss: 6)
Each connected component collapses into a tree structure governed by a single ultimate parent (boss).
4. Naive Traversal vs. Disjoint Set Approach
5. Step-by-Step Algorithm Breakdown
- Initialize DSU: Create a Disjoint Set data structure of size , where each node is initially its own parent.
- Iterate Through Adjacency Matrix: Traverse the matrix using a nested loop (
ifrom0toV-1,jfrom0toV-1). - Union Connected Nodes: If
matrix[i][j] == 1and , performunionBySize(i, j)(orunionByRank). - Count Bosses: Once all edges are processed, iterate through all nodes from
0toV-1. Check how many nodes satisfy the conditionds.findUltimateParent(i) == i. - Return Count: The total number of nodes that are their own ultimate parent equals the number of provinces.
6. Code Implementation (C++ & Java Equivalents)
C++ Solution Implementation
C++
class DisjointSet {
vector<int> rank, parent, size;
public:
DisjointSet(int n) {
rank.resize(n + 1, 0);
parent.resize(n + 1);
size.resize(n + 1);
for (int i = 0; i <= n; i++) {
parent[i] = i;
size[i] = 1;
}
}
int findUltimateParent(int node) {
if (node == parent[node])
return node;
return parent[node] = findUltimateParent(parent[node]); // Path compression
}
void unionByRank(int u, int v) {
int ulp_u = findUltimateParent(u);
int ulp_v = findUltimateParent(v);
if (ulp_u == ulp_v) return;
if (rank[ulp_u] < rank[ulp_v]) {
parent[ulp_u] = ulp_v;
} else if (rank[ulp_v] < rank[ulp_u]) {
parent[ulp_v] = ulp_u;
} else {
parent[ulp_v] = ulp_u;
rank[ulp_u]++;
}
}
void unionBySize(int u, int v) {
int ulp_u = findUltimateParent(u);
int ulp_v = findUltimateParent(v);
if (ulp_u == ulp_v) return;
if (size[ulp_u] < size[ulp_v]) {
parent[ulp_u] = ulp_v;
size[ulp_v] += size[ulp_u];
} else {
parent[ulp_v] = ulp_u;
size[ulp_u] += size[ulp_v];
}
}
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int V = isConnected.size();
DisjointSet ds(V);
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (isConnected[i][j] == 1 && i != j) {
ds.unionBySize(i, j);
}
}
}
int count = 0;
for (int i = 0; i < V; i++) {
if (ds.findUltimateParent(i) == i) {
count++;
}
}
return count;
}
};
7. Complete Flow Walkthrough
Consider a graph with 3 vertices and an adjacency matrix:
CODE
[
[1, 1, 0],
[1, 1, 0],
[0, 0, 1]
]
- Step 1: Initialize
DisjointSetfor . Parents:[0, 1, 2]. - Step 2: Scan adjacency matrix:
- At
i = 0, j = 1,isConnected[0][1] == 1: CallunionBySize(0, 1). Node0and1merge. Parent of0becomes1(or vice versa). - At
i = 1, j = 0,isConnected[1][0] == 1: CallunionBySize(1, 0)(already connected, skipped or no-op). - At
i = 2, j = 2,i == jcondition skips self-loops.
- At
- Step 3: Count ultimate parents:
findUltimateParent(0)-> returns1(since0points to1)findUltimateParent(1)-> returns1(boss of component{0, 1}) ->count++(count = 1)findUltimateParent(2)-> returns2(boss of component{2}) ->count++(count = 2)
- Step 4: Return
count = 2.
8. Complexity Analysis
- Time Complexity:
- Iterating through the adjacency matrix takes time.
- Performing union and find operations takes nearly constant time, , where is the Inverse Ackermann function.
- Total Time Complexity: .
- Space Complexity:
- Storing parent, rank, and size arrays in the Disjoint Set takes extra space.
- Total Space Complexity: .
Summary and Key Takeaways
- Province Definition: The number of provinces in an undirected graph equals the number of connected components.
- Alternative to BFS/DFS: Disjoint Set Data Structure (DSU) provides an alternative, highly modular approach to solving connectivity problems.
- Boss Concept: Every connected component in a DSU forms a tree rooted at a single ultimate parent (the "boss").
- Counting Rule: The total number of provinces is equal to the count of nodes where
findUltimateParent(node) == node. - Matrix Scanning: Since the graph is given as an adjacency matrix, we iterate through all entries and invoke
unionBySizeorunionByRankwhenevermatrix[i][j] == 1. - Path Compression Efficiency: Utilizing path compression guarantees near-constant time operations (), keeping performance optimal.