Before attempting this problem, you should be comfortable with:
Graph Representation (Adjacency Matrix) - Understanding how to represent connections between nodes in a 2D matrix
Depth First Search (DFS) - Recursive traversal technique used to explore all reachable nodes from a starting point
Breadth First Search (BFS) - Level-by-level traversal using a queue to explore connected nodes
Union-Find (Disjoint Set Union) - Data structure for efficiently tracking and merging connected components
1. Depth First Search
Intuition
A province is a group of directly or indirectly connected cities. This is essentially finding the number of connected components in an undirected graph. We can use DFS to explore all cities reachable from a starting city, marking them as visited. Each time we start a new DFS from an unvisited city, we have found a new province.
Algorithm
Create a visited array of size n initialized to false.
Initialize province count res to 0.
For each city i from 0 to n-1:
If city i is not visited:
Increment res (found a new province).
Run dfs from city i to mark all connected cities as visited.
In the dfs:
Mark the current node as visited.
For each neighbor nei that is connected and not visited, recursively call dfs.
classSolution:deffindCircleNum(self, isConnected: List[List[int]])->int:
n =len(isConnected)
res =0
visited =[False]* n
defdfs(node):
visited[node]=Truefor nei inrange(n):if isConnected[node][nei]andnot visited[nei]:
dfs(nei)for i inrange(n):ifnot visited[i]:
dfs(i)
res +=1return res
publicclassSolution{publicintfindCircleNum(int[][] isConnected){int n = isConnected.length;boolean[] visited =newboolean[n];int res =0;for(int i =0; i < n; i++){if(!visited[i]){dfs(i, isConnected, visited, n);
res++;}}return res;}privatevoiddfs(int node,int[][] isConnected,boolean[] visited,int n){
visited[node]=true;for(int nei =0; nei < n; nei++){if(isConnected[node][nei]==1&&!visited[nei]){dfs(nei, isConnected, visited, n);}}}}
classSolution{public:intfindCircleNum(vector<vector<int>>& isConnected){int n = isConnected.size();
vector<bool>visited(n,false);int res =0;for(int i =0; i < n; i++){if(!visited[i]){dfs(i, isConnected, visited, n);
res++;}}return res;}voiddfs(int node, vector<vector<int>>& isConnected, vector<bool>& visited,int n){
visited[node]=true;for(int nei =0; nei < n; nei++){if(isConnected[node][nei]==1&&!visited[nei]){dfs(nei, isConnected, visited, n);}}}};
classSolution{/**
* @param {number[][]} isConnected
* @return {number}
*/findCircleNum(isConnected){const n = isConnected.length;const visited =newArray(n).fill(false);let res =0;constdfs=(node)=>{
visited[node]=true;for(let nei =0; nei < n; nei++){if(isConnected[node][nei]===1&&!visited[nei]){dfs(nei);}}};for(let i =0; i < n; i++){if(!visited[i]){dfs(i);
res++;}}return res;}}
publicclassSolution{publicintFindCircleNum(int[][] isConnected){int n = isConnected.Length;bool[] visited =newbool[n];int res =0;for(int i =0; i < n; i++){if(!visited[i]){Dfs(i, isConnected, visited, n);
res++;}}return res;}privatevoidDfs(int node,int[][] isConnected,bool[] visited,int n){
visited[node]=true;for(int nei =0; nei < n; nei++){if(isConnected[node][nei]==1&&!visited[nei]){Dfs(nei, isConnected, visited, n);}}}}
funcfindCircleNum(isConnected [][]int)int{
n :=len(isConnected)
visited :=make([]bool, n)
res :=0var dfs func(node int)
dfs =func(node int){
visited[node]=truefor nei :=0; nei < n; nei++{if isConnected[node][nei]==1&&!visited[nei]{dfs(nei)}}}for i :=0; i < n; i++{if!visited[i]{dfs(i)
res++}}return res
}
class Solution {funfindCircleNum(isConnected: Array<IntArray>): Int {val n = isConnected.size
val visited =BooleanArray(n)var res =0fundfs(node: Int){
visited[node]=truefor(nei in0 until n){if(isConnected[node][nei]==1&&!visited[nei]){dfs(nei)}}}for(i in0 until n){if(!visited[i]){dfs(i)
res++}}return res
}}
classSolution{funcfindCircleNum(_ isConnected:[[Int]])->Int{let n = isConnected.count
var visited =[Bool](repeating:false, count: n)var res =0funcdfs(_ node:Int){
visited[node]=truefor nei in0..<n {if isConnected[node][nei]==1&&!visited[nei]{dfs(nei)}}}for i in0..<n {if!visited[i]{dfs(i)
res +=1}}return res
}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n)
2. Depth First Search (Modifying Input)
Intuition
Instead of using a separate visited array, we can use the diagonal of the adjacency matrix itself to track visited status. Since isConnected[i][i] is always 1 (a city is connected to itself), we can set it to 0 when we visit that city. This saves space but modifies the input.
Algorithm
Initialize province count res to 0.
For each city i from 0 to n-1:
If isConnected[i][i] equals 1 (not yet visited):
Increment res.
Run dfs from city i.
In the dfs:
Set isConnected[node][node] = 0 to mark as visited.
For each neighbor nei that is connected and whose diagonal is still 1, recursively call dfs.
classSolution:deffindCircleNum(self, isConnected: List[List[int]])->int:
n =len(isConnected)
res =0defdfs(node):
isConnected[node][node]=0for nei inrange(n):if node != nei and isConnected[node][nei]and isConnected[nei][nei]:
dfs(nei)for i inrange(n):if isConnected[i][i]:
dfs(i)
res +=1return res
publicclassSolution{publicintfindCircleNum(int[][] isConnected){int n = isConnected.length;int res =0;for(int i =0; i < n; i++){if(isConnected[i][i]==1){dfs(i, isConnected, n);
res++;}}return res;}privatevoiddfs(int node,int[][] isConnected,int n){
isConnected[node][node]=0;for(int nei =0; nei < n; nei++){if(node != nei && isConnected[node][nei]==1&& isConnected[nei][nei]==1){dfs(nei, isConnected, n);}}}}
classSolution{public:intfindCircleNum(vector<vector<int>>& isConnected){int n = isConnected.size();int res =0;for(int i =0; i < n; i++){if(isConnected[i][i]==1){dfs(i, isConnected, n);
res++;}}return res;}voiddfs(int node, vector<vector<int>>& isConnected,int n){
isConnected[node][node]=0;for(int nei =0; nei < n; nei++){if(node != nei && isConnected[node][nei]==1&& isConnected[nei][nei]==1){dfs(nei, isConnected, n);}}}};
classSolution{/**
* @param {number[][]} isConnected
* @return {number}
*/findCircleNum(isConnected){const n = isConnected.length;let res =0;constdfs=(node)=>{
isConnected[node][node]=0;for(let nei =0; nei < n; nei++){if(node !== nei && isConnected[node][nei]===1&& isConnected[nei][nei]===1){dfs(nei);}}};for(let i =0; i < n; i++){if(isConnected[i][i]===1){dfs(i);
res++;}}return res;}}
publicclassSolution{publicintFindCircleNum(int[][] isConnected){int n = isConnected.Length;int res =0;for(int i =0; i < n; i++){if(isConnected[i][i]==1){Dfs(i, isConnected, n);
res++;}}return res;}privatevoidDfs(int node,int[][] isConnected,int n){
isConnected[node][node]=0;for(int nei =0; nei < n; nei++){if(node != nei && isConnected[node][nei]==1&& isConnected[nei][nei]==1){Dfs(nei, isConnected, n);}}}}
funcfindCircleNum(isConnected [][]int)int{
n :=len(isConnected)
res :=0var dfs func(node int)
dfs =func(node int){
isConnected[node][node]=0for nei :=0; nei < n; nei++{if node != nei && isConnected[node][nei]==1&& isConnected[nei][nei]==1{dfs(nei)}}}for i :=0; i < n; i++{if isConnected[i][i]==1{dfs(i)
res++}}return res
}
class Solution {funfindCircleNum(isConnected: Array<IntArray>): Int {val n = isConnected.size
var res =0fundfs(node: Int){
isConnected[node][node]=0for(nei in0 until n){if(node != nei && isConnected[node][nei]==1&& isConnected[nei][nei]==1){dfs(nei)}}}for(i in0 until n){if(isConnected[i][i]==1){dfs(i)
res++}}return res
}}
classSolution{funcfindCircleNum(_ isConnected:[[Int]])->Int{var isConnected = isConnected
let n = isConnected.count
var res =0funcdfs(_ node:Int){
isConnected[node][node]=0for nei in0..<n {if node != nei && isConnected[node][nei]==1&& isConnected[nei][nei]==1{dfs(nei)}}}for i in0..<n {if isConnected[i][i]==1{dfs(i)
res +=1}}return res
}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n) for recursion stack.
3. Breadth First Search
Intuition
BFS provides an alternative to DFS for exploring connected components. Instead of going deep first, we explore all neighbors at the current level before moving to the next. The logic remains the same: start from an unvisited city, explore all reachable cities using a queue, and count each new starting point as a separate province.
Algorithm
Create a visited array of size n initialized to false.
Initialize province count res to 0 and create an empty queue.
For each city i from 0 to n-1:
If city i is not visited:
Increment res.
Mark city i as visited and add it to the queue.
While the queue is not empty:
Dequeue a city node.
For each neighbor nei that is connected and not visited, mark it visited and enqueue it.
classSolution:deffindCircleNum(self, isConnected: List[List[int]])->int:
n =len(isConnected)
visited =[False]* n
res =0
q = deque()for i inrange(n):ifnot visited[i]:
res +=1
visited[i]=True
q.append(i)while q:
node = q.popleft()for nei inrange(n):if isConnected[node][nei]andnot visited[nei]:
visited[nei]=True
q.append(nei)return res
publicclassSolution{publicintfindCircleNum(int[][] isConnected){int n = isConnected.length;boolean[] visited =newboolean[n];Queue<Integer> q =newLinkedList<>();int res =0;for(int i =0; i < n; i++){if(!visited[i]){
res++;
visited[i]=true;
q.add(i);while(!q.isEmpty()){int node = q.poll();for(int nei =0; nei < n; nei++){if(isConnected[node][nei]==1&&!visited[nei]){
visited[nei]=true;
q.add(nei);}}}}}return res;}}
classSolution{public:intfindCircleNum(vector<vector<int>>& isConnected){int n = isConnected.size();
vector<bool>visited(n,false);
queue<int> q;int res =0;for(int i =0; i < n; i++){if(!visited[i]){
res++;
visited[i]=true;
q.push(i);while(!q.empty()){int node = q.front(); q.pop();for(int nei =0; nei < n; nei++){if(isConnected[node][nei]&&!visited[nei]){
visited[nei]=true;
q.push(nei);}}}}}return res;}};
classSolution{/**
* @param {number[][]} isConnected
* @return {number}
*/findCircleNum(isConnected){const n = isConnected.length;const visited =newArray(n).fill(false);const q =newQueue();let res =0;for(let i =0; i < n; i++){if(!visited[i]){
res++;
visited[i]=true;
q.enqueue(i);while(!q.isEmpty()){const node = q.dequeue();for(let nei =0; nei < n; nei++){if(isConnected[node][nei]&&!visited[nei]){
visited[nei]=true;
q.enqueue(nei);}}}}}return res;}}
publicclassSolution{publicintFindCircleNum(int[][] isConnected){int n = isConnected.Length;bool[] visited =newbool[n];Queue<int> q =newQueue<int>();int res =0;for(int i =0; i < n; i++){if(!visited[i]){
res++;
visited[i]=true;
q.Enqueue(i);while(q.Count >0){int node = q.Dequeue();for(int nei =0; nei < n; nei++){if(isConnected[node][nei]==1&&!visited[nei]){
visited[nei]=true;
q.Enqueue(nei);}}}}}return res;}}
funcfindCircleNum(isConnected [][]int)int{
n :=len(isConnected)
visited :=make([]bool, n)
res :=0for i :=0; i < n; i++{if!visited[i]{
res++
visited[i]=true
q :=[]int{i}forlen(q)>0{
node := q[0]
q = q[1:]for nei :=0; nei < n; nei++{if isConnected[node][nei]==1&&!visited[nei]{
visited[nei]=true
q =append(q, nei)}}}}}return res
}
class Solution {funfindCircleNum(isConnected: Array<IntArray>): Int {val n = isConnected.size
val visited =BooleanArray(n)var res =0for(i in0 until n){if(!visited[i]){
res++
visited[i]=trueval q = ArrayDeque<Int>()
q.add(i)while(q.isNotEmpty()){val node = q.removeFirst()for(nei in0 until n){if(isConnected[node][nei]==1&&!visited[nei]){
visited[nei]=true
q.add(nei)}}}}}return res
}}
classSolution{funcfindCircleNum(_ isConnected:[[Int]])->Int{let n = isConnected.count
var visited =[Bool](repeating:false, count: n)var res =0for i in0..<n {if!visited[i]{
res +=1
visited[i]=truevar q =[i]while!q.isEmpty {let node = q.removeFirst()for nei in0..<n {if isConnected[node][nei]==1&&!visited[nei]{
visited[nei]=true
q.append(nei)}}}}}return res
}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n)
4. Disjoint Set Union
Intuition
The Union-Find (Disjoint Set Union) data structure is designed for efficiently managing connected components. We start with each city as its own component. As we process connections, we union the components of connected cities. The final number of distinct components equals the number of provinces. Path compression and union by size optimizations make this approach nearly O(1) per operation.
Algorithm
Initialize a DSU with n components, where each city is its own parent.
For each pair of cities (i, j) where isConnected[i][j] == 1:
Call union(i, j) to merge their components.
The union operation decrements the component count if the cities were in different components.
type DSU struct{
Parent []int
Size []int
components int}funcNewDSU(n int)*DSU {
dsu :=&DSU{
Parent:make([]int, n),
Size:make([]int, n),
components: n,}for i :=0; i < n; i++{
dsu.Parent[i]= i
dsu.Size[i]=1}return dsu
}func(dsu *DSU)Find(node int)int{if dsu.Parent[node]!= node {
dsu.Parent[node]= dsu.Find(dsu.Parent[node])}return dsu.Parent[node]}func(dsu *DSU)Union(u, v int)bool{
pu, pv := dsu.Find(u), dsu.Find(v)if pu == pv {returnfalse}
dsu.components--if dsu.Size[pu]>= dsu.Size[pv]{
dsu.Size[pu]+= dsu.Size[pv]
dsu.Parent[pv]= pu
}else{
dsu.Size[pv]+= dsu.Size[pu]
dsu.Parent[pu]= pv
}returntrue}funcfindCircleNum(isConnected [][]int)int{
n :=len(isConnected)
dsu :=NewDSU(n)for i :=0; i < n; i++{for j :=0; j < n; j++{if isConnected[i][j]==1{
dsu.Union(i, j)}}}return dsu.components
}
classDSU(n: Int){privateval parent =IntArray(n){ it }privateval size =IntArray(n){1}var components = n
privatesetfunfind(node: Int): Int {if(parent[node]!= node){
parent[node]=find(parent[node])}return parent[node]}fununion(u: Int, v: Int): Boolean {val pu =find(u)val pv =find(v)if(pu == pv)returnfalse
components--if(size[pu]>= size[pv]){
size[pu]+= size[pv]
parent[pv]= pu
}else{
size[pv]+= size[pu]
parent[pu]= pv
}returntrue}}class Solution {funfindCircleNum(isConnected: Array<IntArray>): Int {val n = isConnected.size
val dsu =DSU(n)for(i in0 until n){for(j in0 until n){if(isConnected[i][j]==1){
dsu.union(i, j)}}}return dsu.components
}}
classDSU{privatevar parent:[Int]privatevar size:[Int]var components:Intinit(_ n:Int){
parent =Array(0..<n)
size =[Int](repeating:1, count: n)
components = n
}funcfind(_ node:Int)->Int{if parent[node]!= node {
parent[node]=find(parent[node])}return parent[node]}funcunion(_ u:Int,_ v:Int)->Bool{let pu =find(u)let pv =find(v)if pu == pv {returnfalse}
components -=1if size[pu]>= size[pv]{
size[pu]+= size[pv]
parent[pv]= pu
}else{
size[pv]+= size[pu]
parent[pu]= pv
}returntrue}}classSolution{funcfindCircleNum(_ isConnected:[[Int]])->Int{let n = isConnected.count
let dsu =DSU(n)for i in0..<n {for j in0..<n {if isConnected[i][j]==1{_= dsu.union(i, j)}}}return dsu.components
}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n)
Common Pitfalls
Confusing Nodes with Edges
The adjacency matrix isConnected[i][j] represents whether city i and city j are directly connected. A common mistake is treating the matrix indices as edge indices rather than node indices. Remember that n is the number of cities, not the number of connections.
Forgetting to Mark Nodes as Visited
When performing DFS or BFS, failing to mark a node as visited before or immediately after processing it can lead to infinite loops or counting the same province multiple times. Always mark nodes as visited as soon as they are encountered.
Misunderstanding the Diagonal
The diagonal elements isConnected[i][i] are always 1 (a city is connected to itself). When modifying the input to track visited status, be careful to use the diagonal correctly. Do not confuse self-connections with actual inter-city connections.