LeetCode 547 - Number of Provinces
The problem is asking us to determine the number of provinces in a network of cities. Each city can be connected directly to other cities, and indirectly through chains of connections.
Difficulty: 🟡 Medium
Topics: Depth-First Search, Breadth-First Search, Union-Find, Graph Theory
Solution
Problem Understanding
The problem is asking us to determine the number of provinces in a network of cities. Each city can be connected directly to other cities, and indirectly through chains of connections. A province is defined as a set of cities that are all directly or indirectly connected, and that has no connections to cities outside the set.
The input is an n x n adjacency matrix isConnected, where isConnected[i][j] is 1 if city i and city j are directly connected, and 0 otherwise. Each city is always connected to itself, so the diagonal of the matrix is always 1. The output is a single integer representing the total number of provinces.
Constraints indicate that n can be up to 200, so a solution with O(n^3) complexity might be borderline acceptable, but O(n^2) or better is preferable. The matrix is symmetric, and cities are always connected to themselves, which simplifies traversal and ensures we do not need to check for invalid indices.
Important edge cases include fully connected cities (1 province), completely disconnected cities (each city is its own province), and the minimal case of a single city (1 province).
Approaches
The brute-force approach would be to consider each city and attempt to traverse all paths to other cities recursively or iteratively, keeping track of which cities have been visited. For each unvisited city, we would start a traversal to mark all cities in its province. This works because each province corresponds to a connected component in the graph. However, naive implementations may repeatedly check connections, resulting in O(n^3) time complexity if recursion explores all paths multiple times.
The optimal approach uses either Depth-First Search (DFS), Breadth-First Search (BFS), or Union-Find. These methods efficiently identify connected components:
- DFS/BFS: For each unvisited city, perform DFS or BFS to mark all reachable cities as visited. Each traversal identifies exactly one province. Time complexity is
O(n^2)because every matrix cell may be visited at most once. - Union-Find: Treat each city as a node in a disjoint-set forest. Union connected cities. The number of disjoint sets at the end is the number of provinces. Union-Find with path compression achieves nearly linear time in practice,
O(n^2 * α(n)).
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^3) | O(n) | Traversal from each city with repeated visits leads to higher time complexity |
| DFS / BFS | O(n^2) | O(n) | Visits each city and edge once |
| Union-Find | O(n^2 * α(n)) | O(n) | Uses disjoint-set operations, efficient for repeated unions |
Algorithm Walkthrough
We will describe the DFS approach in detail:
-
Initialize a
visitedlist of lengthnto track which cities have been explored. -
Initialize a
province_countto 0. -
Iterate over each city
ifrom0ton-1. -
If city
ihas not been visited: -
Increment
province_countby 1 because starting a DFS here means a new province. -
Perform DFS starting at city
i:
- Mark the city as visited.
- For each city
j, ifisConnected[i][j] == 1andjis not visited, recursively visit cityj.
- After iterating all cities, return
province_count.
Why it works: Each DFS traversal fully explores a connected component of the graph. Since each province corresponds to a connected component, counting DFS starts on unvisited cities counts all provinces exactly once.
Python Solution
from typing import List
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
n = len(isConnected)
visited = [False] * n
def dfs(city: int):
visited[city] = True
for neighbor in range(n):
if isConnected[city][neighbor] == 1 and not visited[neighbor]:
dfs(neighbor)
province_count = 0
for i in range(n):
if not visited[i]:
province_count += 1
dfs(i)
return province_count
This implementation first creates a visited array to keep track of explored cities. The dfs function marks the current city as visited and recursively visits all directly connected, unvisited cities. The main loop increments province_count whenever it finds an unvisited city, ensuring each connected component is counted once.
Go Solution
func findCircleNum(isConnected [][]int) int {
n := len(isConnected)
visited := make([]bool, n)
var dfs func(int)
dfs = func(city int) {
visited[city] = true
for neighbor := 0; neighbor < n; neighbor++ {
if isConnected[city][neighbor] == 1 && !visited[neighbor] {
dfs(neighbor)
}
}
}
provinceCount := 0
for i := 0; i < n; i++ {
if !visited[i] {
provinceCount++
dfs(i)
}
}
return provinceCount
}
The Go version mirrors the Python implementation, using slices for visited and a recursive dfs function defined as a closure to allow recursion.
Worked Examples
Example 1:
Input: [[1,1,0],[1,1,0],[0,0,1]]
Step through DFS:
| City | Visited | Action |
|---|---|---|
| 0 | [True, False, False] | DFS to 1 |
| 1 | [True, True, False] | DFS ends |
| 2 | [True, True, True] | DFS for new province |
Output: 2
Example 2:
Input: [[1,0,0],[0,1,0],[0,0,1]]
All cities are disconnected, each DFS finds one province:
Output: 3
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n^2) | Each cell in the adjacency matrix is visited at most once in DFS |
| Space | O(n) | Visited array and call stack for recursion up to n depth |
Test Cases
# Provided examples
assert Solution().findCircleNum([[1,1,0],[1,1,0],[0,0,1]]) == 2 # two provinces
assert Solution().findCircleNum([[1,0,0],[0,1,0],[0,0,1]]) == 3 # three provinces
# Single city
assert Solution().findCircleNum([[1]]) == 1
# Fully connected cities
assert Solution().findCircleNum([[1,1,1],[1,1,1],[1,1,1]]) == 1
# Two provinces
assert Solution().findCircleNum([[1,1,0,0],[1,1,0,0],[0,0,1,1],[0,0,1,1]]) == 2
# Disconnected city in middle
assert Solution().findCircleNum([[1,1,0],[1,1,0],[0,0,1]]) == 2
| Test | Why |
|---|---|
| [[1,1,0],[1,1,0],[0,0,1]] | Tests basic connectivity and indirect connections |
| [[1,0,0],[0,1,0],[0,0,1]] | Tests completely disconnected cities |
| [[1]] | Tests minimal input |
| [[1,1,1],[1,1,1],[1,1,1]] | Tests fully connected graph, 1 province |
| [[1,1,0,0],[1,1,0,0],[0,0,1,1],[0,0,1,1]] | Tests multiple separate components |
| [[1,1,0],[1,1,0],[0,0,1]] | Repetition of basic example, ensures algorithm consistency |
Edge Cases
Single City: When n=1, the algorithm correctly returns 1 since the DFS is invoked once and terminates immediately.
Completely Disconnected Graph: When all cities are disconnected except self-connections, each city forms its own province. DFS ensures each city is counted separately, and no recursion goes beyond the single city.
Fully Connected Graph: When all cities are connected to each other, DFS visits every city starting from the first one. Subsequent iterations skip visited cities, ensuring the province count is exactly 1. This validates that the implementation does not overcount.