547. Number of Provinces
Problem Description
You have n
cities and need to find how many provinces exist among them. A province is defined as a group of cities that are connected either directly or indirectly.
The connections between cities are given in an n x n
matrix called isConnected
:
isConnected[i][j] = 1
means cityi
and cityj
are directly connectedisConnected[i][j] = 0
means cityi
and cityj
are not directly connected
Cities can be connected indirectly through other cities. For example, if city a
connects to city b
, and city b
connects to city c
, then city a
is indirectly connected to city c
. All such connected cities (directly or indirectly) form a single province.
The task is to count the total number of provinces. Each province is an isolated group of connected cities with no connections to cities outside that group.
The solution uses depth-first search (DFS) to explore connected cities. Starting from each unvisited city, it performs DFS to mark all cities in the same province as visited. The vis
array tracks which cities have been visited. For each unvisited city i
, the algorithm:
- Starts a DFS from that city
- Marks all connected cities (directly or indirectly) as visited
- Increments the province count by 1
The DFS function recursively visits all cities connected to the current city by checking the isConnected
matrix. When DFS completes for a starting city, all cities in that province are marked as visited. The process continues until all cities are visited, and the total count of provinces is returned.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves cities (nodes) and connections between them (edges). The
isConnected
matrix represents an adjacency matrix of a graph where cities are vertices and connections are edges.
Is it a tree?
- No: The graph is not necessarily a tree. It can have multiple disconnected components (provinces), and within each component, there might be cycles. A tree would have exactly
n-1
edges forn
nodes and be fully connected, which is not the case here.
Is the problem related to directed acyclic graphs?
- No: The connections are bidirectional (if city
i
connects to cityj
, then cityj
connects to cityi
), making this an undirected graph. Also, the graph may contain cycles.
Is the problem related to shortest paths?
- No: We're not finding the shortest path between cities. We're counting the number of disconnected components (provinces) in the graph.
Does the problem involve connectivity?
- Yes: The core of the problem is about connectivity - finding groups of cities that are connected directly or indirectly. Each group forms a province.
Disjoint Set Union
- While DSU could solve this problem, we can also use DFS. Since we need to traverse all connected cities starting from each unvisited city, DFS is a natural choice.
Conclusion: The flowchart leads us to connectivity problems, which can be solved using either DSU or DFS. The solution uses DFS to explore all cities within each province, marking them as visited, and counting the number of times we need to start a new DFS (which equals the number of provinces).
Intuition
The key insight is recognizing that this is a graph connectivity problem where we need to count disconnected components. Each province is essentially a connected component in an undirected graph.
Think of it this way: if we start from any unvisited city and explore all cities we can reach from it (directly or indirectly), we'll have found one complete province. The cities within a province form a connected component - you can travel between any two cities in the same province through some path of connections.
The natural approach is to:
- Pick an unvisited city
- Explore all cities reachable from it (marking them as visited)
- Count this as one province
- Repeat until all cities are visited
This is exactly what DFS does - it exhaustively explores all nodes reachable from a starting node. When we start a DFS from city i
, it will visit city i
and recursively visit all cities connected to i
, then all cities connected to those cities, and so on. This ensures we visit every city in the current province before the DFS completes.
The number of times we need to initiate a new DFS equals the number of provinces. If all cities were in one province, we'd only need one DFS call. If each city was isolated (its own province), we'd need n
DFS calls.
The vis
array prevents us from counting cities multiple times - once a city is assigned to a province, we never need to visit it again. This ensures each city belongs to exactly one province, and we correctly count the total number of provinces.
Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
The implementation uses Depth-First Search (DFS) to find all connected components in the graph. Here's how the solution works:
Data Structures:
vis
: A boolean array of sizen
to track which cities have been visited. Initially, all values areFalse
.ans
: Counter variable to store the number of provinces found.
Algorithm Steps:
-
Define the DFS function: The
dfs(i)
function explores all cities connected to cityi
:- Mark city
i
as visited:vis[i] = True
- Iterate through all cities
j
usingenumerate(isConnected[i])
- For each city
j
, check if:- It hasn't been visited yet:
not vis[j]
- It's directly connected to city
i
:x
(which isisConnected[i][j]
) equals 1
- It hasn't been visited yet:
- If both conditions are true, recursively call
dfs(j)
to explore cityj
and all its connections
- Mark city
-
Main loop to count provinces:
- Iterate through all cities from
0
ton-1
- For each city
i
, check if it hasn't been visited:not vis[i]
- If unvisited:
- Call
dfs(i)
to mark this city and all cities in its province as visited - Increment the province counter:
ans += 1
- Call
- This ensures each new DFS call represents discovering a new province
- Iterate through all cities from
-
Return the result: After checking all cities,
ans
contains the total number of provinces.
Why this works:
- Each DFS call from the main loop discovers exactly one province (connected component)
- The DFS explores all cities reachable from the starting city, marking them as visited
- Cities in the same province won't trigger new DFS calls since they're already marked as visited
- The number of DFS calls initiated from the main loop equals the number of provinces
Time Complexity: O(nΒ²)
where n
is the number of cities, as we potentially visit every cell in the n Γ n
matrix.
Space Complexity: O(n)
for the vis
array and the recursion stack in the worst case.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with 4 cities and the following connections:
isConnected = [[1,1,0,0], [1,1,1,0], [0,1,1,0], [0,0,0,1]]
This represents:
- City 0 connects to: itself and city 1
- City 1 connects to: itself, city 0, and city 2
- City 2 connects to: itself and city 1
- City 3 connects to: only itself (isolated)
Initial State:
vis = [False, False, False, False]
ans = 0
Step 1: Check city 0
- City 0 is not visited, so start DFS from city 0
- DFS(0):
- Mark city 0 as visited:
vis = [True, False, False, False]
- Check connections:
isConnected[0] = [1,1,0,0]
- City 1 is connected (value=1) and unvisited, so call DFS(1)
- DFS(1):
- Mark city 1 as visited:
vis = [True, True, False, False]
- Check connections:
isConnected[1] = [1,1,1,0]
- City 0 is connected but already visited (skip)
- City 2 is connected (value=1) and unvisited, so call DFS(2)
- DFS(2):
- Mark city 2 as visited:
vis = [True, True, True, False]
- Check connections:
isConnected[2] = [0,1,1,0]
- City 1 is connected but already visited (skip)
- No more unvisited connected cities
- Mark city 2 as visited:
- DFS(2) completes and returns to DFS(1)
- Mark city 1 as visited:
- DFS(1) completes and returns to DFS(0)
- Mark city 0 as visited:
- DFS(0) completes
- Increment province count:
ans = 1
Step 2: Check city 1
- City 1 is already visited (skip)
Step 3: Check city 2
- City 2 is already visited (skip)
Step 4: Check city 3
- City 3 is not visited, so start DFS from city 3
- DFS(3):
- Mark city 3 as visited:
vis = [True, True, True, True]
- Check connections:
isConnected[3] = [0,0,0,1]
- Only connected to itself, no other unvisited cities
- Mark city 3 as visited:
- DFS(3) completes
- Increment province count:
ans = 2
Result: The algorithm found 2 provinces:
- Province 1: Cities {0, 1, 2} - all interconnected
- Province 2: City {3} - isolated
The final answer is 2 provinces.
Solution Implementation
1class Solution:
2 def findCircleNum(self, isConnected: List[List[int]]) -> int:
3 """
4 Find the number of provinces (connected components) in the graph.
5 A province is a group of directly or indirectly connected cities.
6
7 Args:
8 isConnected: An n x n matrix where isConnected[i][j] = 1 if cities i and j are connected
9
10 Returns:
11 The total number of provinces
12 """
13
14 def dfs(city_index: int) -> None:
15 """
16 Depth-first search to mark all cities in the same province as visited.
17
18 Args:
19 city_index: The index of the current city being explored
20 """
21 # Mark the current city as visited
22 visited[city_index] = True
23
24 # Explore all neighboring cities
25 for neighbor_index, is_connected_to_neighbor in enumerate(isConnected[city_index]):
26 # If the neighbor hasn't been visited and is connected to current city
27 if not visited[neighbor_index] and is_connected_to_neighbor:
28 dfs(neighbor_index)
29
30 # Get the number of cities
31 num_cities = len(isConnected)
32
33 # Initialize visited array to track which cities have been explored
34 visited = [False] * num_cities
35
36 # Counter for the number of provinces
37 province_count = 0
38
39 # Iterate through all cities
40 for city_index in range(num_cities):
41 # If the city hasn't been visited, it's part of a new province
42 if not visited[city_index]:
43 # Explore all cities in this province
44 dfs(city_index)
45 # Increment the province count
46 province_count += 1
47
48 return province_count
49
1class Solution {
2 // Graph adjacency matrix representing connections
3 private int[][] graph;
4 // Visited array to track which nodes have been explored
5 private boolean[] visited;
6
7 /**
8 * Finds the number of connected components (provinces) in the graph.
9 * A province is a group of directly or indirectly connected cities.
10 *
11 * @param isConnected 2D array where isConnected[i][j] = 1 means city i and j are connected
12 * @return The total number of provinces
13 */
14 public int findCircleNum(int[][] isConnected) {
15 // Initialize the graph with the input matrix
16 graph = isConnected;
17 int n = graph.length;
18 visited = new boolean[n];
19
20 int provinceCount = 0;
21
22 // Iterate through each city
23 for (int i = 0; i < n; i++) {
24 // If the city hasn't been visited, it's part of a new province
25 if (!visited[i]) {
26 // Explore all cities in this province using DFS
27 dfs(i);
28 // Increment province count after exploring the entire component
29 provinceCount++;
30 }
31 }
32
33 return provinceCount;
34 }
35
36 /**
37 * Depth-First Search to explore all cities connected to the current city.
38 * Marks all reachable cities as visited.
39 *
40 * @param currentCity The current city index being explored
41 */
42 private void dfs(int currentCity) {
43 // Mark current city as visited
44 visited[currentCity] = true;
45
46 // Check all other cities for connections
47 for (int nextCity = 0; nextCity < graph.length; nextCity++) {
48 // If the city hasn't been visited and is connected to current city
49 if (!visited[nextCity] && graph[currentCity][nextCity] == 1) {
50 // Recursively visit the connected city
51 dfs(nextCity);
52 }
53 }
54 }
55}
56
1class Solution {
2public:
3 int findCircleNum(vector<vector<int>>& isConnected) {
4 int n = isConnected.size();
5 int provinceCount = 0;
6 vector<bool> visited(n, false); // Track visited cities
7
8 // Define DFS function to explore all cities in a province
9 function<void(int)> dfs = [&](int city) -> void {
10 visited[city] = true;
11
12 // Visit all directly connected cities
13 for (int neighbor = 0; neighbor < n; ++neighbor) {
14 if (!visited[neighbor] && isConnected[city][neighbor] == 1) {
15 dfs(neighbor);
16 }
17 }
18 };
19
20 // Count the number of provinces (connected components)
21 for (int i = 0; i < n; ++i) {
22 if (!visited[i]) {
23 // Found a new province, explore all cities in it
24 dfs(i);
25 provinceCount++;
26 }
27 }
28
29 return provinceCount;
30 }
31};
32
1/**
2 * Finds the number of connected components (provinces) in the graph
3 * @param isConnected - 2D adjacency matrix where isConnected[i][j] = 1 means cities i and j are connected
4 * @returns The total number of provinces (connected components)
5 */
6function findCircleNum(isConnected: number[][]): number {
7 const cityCount: number = isConnected.length;
8 const visited: boolean[] = Array(cityCount).fill(false);
9
10 /**
11 * Depth-first search to mark all cities connected to the current city as visited
12 * @param currentCity - The index of the current city being explored
13 */
14 const depthFirstSearch = (currentCity: number): void => {
15 // Mark the current city as visited
16 visited[currentCity] = true;
17
18 // Explore all neighboring cities
19 for (let neighborCity = 0; neighborCity < cityCount; neighborCity++) {
20 // Visit unvisited neighbors that are directly connected
21 if (!visited[neighborCity] && isConnected[currentCity][neighborCity] === 1) {
22 depthFirstSearch(neighborCity);
23 }
24 }
25 };
26
27 let provinceCount: number = 0;
28
29 // Iterate through all cities to find disconnected provinces
30 for (let city = 0; city < cityCount; city++) {
31 // If the city hasn't been visited, it's part of a new province
32 if (!visited[city]) {
33 // Explore all cities connected to this one
34 depthFirstSearch(city);
35 // Increment the province counter
36 provinceCount++;
37 }
38 }
39
40 return provinceCount;
41}
42
Time and Space Complexity
Time Complexity: O(nΒ²)
The algorithm uses depth-first search (DFS) to find connected components in the graph. In the worst case:
- The outer loop in the main function iterates through all
n
cities:O(n)
- For each unvisited city, DFS is called, which can potentially visit all
n
cities - Within each DFS call, we iterate through the adjacency list of the current city, which has
n
elements:O(n)
per DFS call - Overall, each element in the
n Γ n
adjacency matrixisConnected
is examined at most once throughout the entire algorithm
Therefore, the total time complexity is O(nΒ²)
, where n
is the number of cities.
Space Complexity: O(n)
The space usage consists of:
- The
vis
array of sizen
to track visited cities:O(n)
- The recursive call stack for DFS, which in the worst case (a linear chain of connected cities) can go up to depth
n
:O(n)
- A few auxiliary variables (
ans
,i
,j
):O(1)
The dominant factor is O(n)
from both the visited array and the potential recursion depth, giving us a total space complexity of O(n)
, where n
is the number of cities.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Matrix Representation
A common mistake is assuming that isConnected[i][j] = 1
only when i != j
. However, the matrix includes self-connections where isConnected[i][i] = 1
for all cities (a city is connected to itself). This could lead to incorrect implementations if not handled properly.
Solution: The current DFS implementation naturally handles this because when we visit a city, we mark it as visited immediately, preventing infinite loops from self-connections.
2. Forgetting to Check Both Visited Status AND Connection
Some implementations might check only if a city is connected but forget to verify if it's already been visited, leading to infinite recursion or redundant processing.
Incorrect approach:
def dfs(i):
vis[i] = True
for j, x in enumerate(isConnected[i]):
if x: # Only checking connection, not visited status
dfs(j)
Solution: Always check both conditions:
if not visited[neighbor_index] and is_connected_to_neighbor: dfs(neighbor_index)
3. Using the Wrong Data Structure for Tracking Visited Cities
Using a set to track visited cities and accidentally checking membership incorrectly:
Problematic approach:
visited = set()
# Later in code...
if i not in visited: # Correct
dfs(i)
# But in DFS...
if not visited[j]: # Wrong! This treats set as a list
Solution: If using a set, be consistent with set operations (add()
, in
). If using a list, use boolean indexing consistently.
4. Modifying the Input Matrix
Some might try to mark visited cities by modifying isConnected
directly to save space:
def dfs(i):
for j in range(n):
if isConnected[i][j] == 1:
isConnected[i][j] = 0 # Modifying input
isConnected[j][i] = 0 # Breaking symmetry
dfs(j)
This approach has multiple issues:
- It destroys the input data
- It might not mark all necessary cells (missing the starting city's connections)
- It's harder to debug
Solution: Use a separate visited
array to track explored cities without modifying the input.
5. Inefficient Enumeration Pattern
Using range(len()) instead of enumerate when you need both index and value:
Less efficient:
for j in range(len(isConnected[i])):
if not visited[j] and isConnected[i][j]:
Better approach:
for j, is_connected in enumerate(isConnected[i]):
if not visited[j] and is_connected:
The enumerate approach is cleaner and more Pythonic, avoiding repeated indexing.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!