There are n
cities. Some of them are connected, while some are not. If city a
is connected directly with city b
, and city b
is connected directly with city c
, then city a
is connected indirectly with city c
.
A province is a group of directly or indirectly connected cities and no other cities outside of the group.
You are given an n x n
matrix isConnected
where isConnected[i][j] = 1
if the ith
city and the jth
city are directly connected, and isConnected[i][j] = 0
otherwise.
Return the total number of provinces.
Example 1:
Input: isConnected = [[1,1,0],[1,1,0],[0,0,1]] Output: 2
Example 2:
Input: isConnected = [[1,0,0],[0,1,0],[0,0,1]] Output: 3
Constraints:
1 <= n <= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j]
is 1
or 0
.isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
The problem asks to find the number of provinces in a given graph represented by an adjacency matrix isConnected
. A province is a group of directly or indirectly connected cities. Two cities are directly connected if isConnected[i][j] == 1
.
Two common approaches to solve this problem are Depth-First Search (DFS) and Union-Find.
This approach uses DFS to traverse the graph and identify connected components.
Algorithm:
Initialization: Create a boolean array vis
of size n
(number of cities), initialized to false
. This array tracks visited cities. Initialize a counter ans
to 0, representing the number of provinces.
Iteration: Iterate through each city i
from 0 to n-1
.
DFS Check: If vis[i]
is false
(city i
is not visited), it means a new province is found. Increment ans
. Call a recursive DFS function starting from city i
.
DFS Function: The DFS function dfs(i)
marks vis[i]
as true
. Then, it iterates through the neighbors of city i
(cities j
where isConnected[i][j] == 1
). If a neighbor j
is not visited (vis[j]
is false
), it recursively calls dfs(j)
to explore that neighbor's connected components.
Time Complexity: O(N^2), where N is the number of cities. This is because we visit each city and its neighbors once. In the worst case, we might traverse all the edges of the graph.
Space Complexity: O(N) to store the vis
array. The recursive call stack in DFS also contributes to space complexity, but it's bounded by the height of the graph, which is at most N in this case.
Code (Python):
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
n = len(isConnected)
vis = [False] * n
ans = 0
def dfs(i: int):
vis[i] = True
for j in range(n):
if isConnected[i][j] and not vis[j]:
dfs(j)
for i in range(n):
if not vis[i]:
dfs(i)
ans += 1
return ans
Similar code can be written in other languages like Java, C++, JavaScript, etc., following the same algorithm.
Union-find is a disjoint-set data structure that efficiently tracks connected components.
Algorithm:
Initialization: Create a parent array p
of size n
, where initially p[i] = i
(each city is its own parent, representing separate components). Initialize a count ans
to n
(initially, each city is a separate province).
Iteration: Iterate through the isConnected
matrix.
Union: If isConnected[i][j] == 1
, find the root parents of i
and j
using a find
function (with path compression for optimization). If the roots are different, it means i
and j
belong to different components. Perform a union operation by setting p[root_of_i] = root_of_j
, merging the components. Decrement ans
because we've merged two provinces.
Find Function: The find
function recursively finds the root parent of a node, applying path compression to optimize subsequent find
operations.
Time Complexity: O(N^2 * α(N)), where α(N) is the inverse Ackermann function, which grows extremely slowly and is practically a constant. The O(N^2) factor comes from iterating through the adjacency matrix. Path compression significantly improves the performance of Union-Find.
Space Complexity: O(N) to store the p
array.
Code (Python):
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
n = len(isConnected)
p = list(range(n))
ans = n
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x]) # Path compression
return p[x]
for i in range(n):
for j in range(i + 1, n):
if isConnected[i][j]:
root_i = find(i)
root_j = find(j)
if root_i != root_j:
p[root_i] = root_j
ans -= 1
return ans
Again, similar code can be written in other languages.
Choosing between DFS and Union-Find:
For this specific problem, DFS is often slightly simpler to implement. Union-Find's advantage becomes more apparent in problems involving more complex queries on connected components. The asymptotic time complexities are similar in practice, with Union-Find possibly having a slight edge due to path compression.