🧠
Union-Find (Disjoint Set Union) Approach
💡 Union-Find is a classic data structure to efficiently track connected components. This approach reduces repeated traversals by merging connected cities into sets.
Intuition
We treat each city as a node in a disjoint set. For every connection, we union the sets of the two cities. The number of unique parents after all unions is the number of provinces.
Algorithm
- Initialize parent array where each city is its own parent.
- Iterate over the upper triangle of the matrix to avoid duplicate checks.
- For each connected pair, union their sets.
- Count how many cities are their own parent to find number of provinces.
💡 Union-Find avoids repeated DFS by merging sets, making connectivity checks faster and simpler.
from typing import List
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.count = n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX != rootY:
if self.rank[rootX] < self.rank[rootY]:
self.parent[rootX] = rootY
elif self.rank[rootX] > self.rank[rootY]:
self.parent[rootY] = rootX
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1
self.count -= 1
def findCircleNum(isConnected: List[List[int]]) -> int:
n = len(isConnected)
uf = UnionFind(n)
for i in range(n):
for j in range(i + 1, n):
if isConnected[i][j] == 1:
uf.union(i, j)
return uf.count
# Driver code
if __name__ == '__main__':
matrix = [[1,1,0],[1,1,0],[0,0,1]]
print(findCircleNum(matrix)) # Output: 2
Line Notes
self.parent = list(range(n))Initialize each city as its own parent
if self.parent[x] != x:Path compression to flatten tree for faster finds
if rootX != rootY:Only union if roots differ to avoid cycles
self.count -= 1Decrease province count when two sets merge
public class NumberOfProvinces {
static class UnionFind {
int[] parent, rank;
int count;
UnionFind(int n) {
parent = new int[n];
rank = new int[n];
count = n;
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] < rank[rootY]) parent[rootX] = rootY;
else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX;
else {
parent[rootY] = rootX;
rank[rootX]++;
}
count--;
}
}
}
public static int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
UnionFind uf = new UnionFind(n);
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (isConnected[i][j] == 1) uf.union(i, j);
}
}
return uf.count;
}
public static void main(String[] args) {
int[][] matrix = {{1,1,0},{1,1,0},{0,0,1}};
System.out.println(findCircleNum(matrix)); // Output: 2
}
}
Line Notes
parent = new int[n];Each city starts as its own parent
if (parent[x] != x) parent[x] = find(parent[x]);Path compression for efficient find
if (rootX != rootY)Union only if roots differ to merge sets
count--;Decrement count when two sets merge
#include <iostream>
#include <vector>
using namespace std;
class UnionFind {
public:
vector<int> parent, rank;
int count;
UnionFind(int n) : parent(n), rank(n, 0), count(n) {
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unionSet(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] < rank[rootY]) parent[rootX] = rootY;
else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX;
else {
parent[rootY] = rootX;
rank[rootX]++;
}
count--;
}
}
};
int findCircleNum(const vector<vector<int>>& isConnected) {
int n = isConnected.size();
UnionFind uf(n);
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (isConnected[i][j] == 1) uf.unionSet(i, j);
}
}
return uf.count;
}
int main() {
vector<vector<int>> matrix = {{1,1,0},{1,1,0},{0,0,1}};
cout << findCircleNum(matrix) << endl; // Output: 2
return 0;
}
Line Notes
parent(n), rank(n, 0), count(n)Initialize parent and rank arrays and count provinces
if (parent[x] != x) parent[x] = find(parent[x]);Path compression to speed up find
if (rootX != rootY)Union only if roots differ to merge sets
count--;Reduce province count when union merges two sets
class UnionFind {
constructor(n) {
this.parent = Array.from({length: n}, (_, i) => i);
this.rank = new Array(n).fill(0);
this.count = n;
}
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
union(x, y) {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX !== rootY) {
if (this.rank[rootX] < this.rank[rootY]) {
this.parent[rootX] = rootY;
} else if (this.rank[rootX] > this.rank[rootY]) {
this.parent[rootY] = rootX;
} else {
this.parent[rootY] = rootX;
this.rank[rootX]++;
}
this.count--;
}
}
}
function findCircleNum(isConnected) {
const n = isConnected.length;
const uf = new UnionFind(n);
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
if (isConnected[i][j] === 1) uf.union(i, j);
}
}
return uf.count;
}
// Test
const matrix = [[1,1,0],[1,1,0],[0,0,1]];
console.log(findCircleNum(matrix)); // Output: 2
Line Notes
this.parent = Array.from({length: n}, (_, i) => i);Initialize each city as its own parent
if (this.parent[x] !== x)Path compression to flatten tree for faster find
if (rootX !== rootY)Union only if roots differ to merge sets
this.count--;Decrement count when two sets merge
TimeO(n^2 * α(n))
SpaceO(n)
We iterate over the upper triangle of the matrix (O(n^2)) and perform union/find operations which are almost O(1) amortized (α(n) is inverse Ackermann function). Space is O(n) for parent and rank arrays.
💡 For n=1000, this is about 1,000,000 operations but union-find operations are very fast, making this approach efficient.
Interview Verdict: Accepted and efficient for large inputs
Union-Find is the preferred approach in interviews for this problem due to its efficiency and clarity in managing connected components.