DSA sheet · Graphs · Connected components pattern

Number of Provinces

This is the first real graph question in the sheet, and it teaches the single most useful graph idea: counting connected components. The teacher shows that the input is already a ready-made adjacency matrix, that one DFS call colours a whole group as "visited", and that the number of times we have to start a fresh DFS is the answer. She codes it with DFS and explains what changes for BFS. The same loop ("for every node, if not visited, start a traversal and count it") comes back in Number of Islands, Flood Fill, Surrounded Regions and many more.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Graph basics this page needs

Nodes and edges

A graph is a set of points joined by lines. The points are called nodes (or vertices). The lines are called edges. In this problem every city is a node and every direct road between two cities is an edge.

(1)—(2)—(3)        (4)—(5)
 nodes: 1 2 3 4 5     edges: 1-2, 2-3, 4-5

Directed, undirected, weighted

kindmeaningdrawn as
undirectedThe edge works both ways. If 1 is joined to 2, then 2 is joined to 1. Provinces is undirected: a road between two cities can be used in both directions.(1)—(2)
directedThe edge is a one-way street: you can go from 1 to 2 but not back (unless there is a second edge 2 → 1).(1)→(2)
weightedEach edge carries a number, such as a distance or a cost. Provinces has no weights: an edge either exists or not.(1)—5—(2)

Three ways the input can arrive

formwhat it looks like (for the picture above, cities 1..5 stored as 0..4)how to find the neighbours of node u
edge list[[0,1],[1,2],[3,4]]: one pair per edgeyou must scan every edge, so first convert it to an adjacency list
adjacency matrixan n × n grid of 0/1. M[u][v] = 1 means u and v are joined.walk along row u and pick every column v with a 1
adjacency list[[1],[0,2],[1],[4],[3]]: list number u holds u's neighboursjust read adj[u]

Most graph problems hand you an edge list, and the first job is to build an adjacency list, because it gives the neighbours of a node directly. Here is how in Python:

building an adjacency list (undirected)
n = 5
edges = [[0, 1], [1, 2], [3, 4]]

adj = [[] for _ in range(n)]     # one empty list per node
for u, v in edges:
    adj[u].append(v)             # u can reach v
    adj[v].append(u)             # undirected: v can reach u too
print(adj)                       # [[1], [0, 2], [1], [4], [3]]

# the same list built from an adjacency matrix
M = [[1, 1, 0, 0, 0],
     [1, 1, 1, 0, 0],
     [0, 1, 1, 0, 0],
     [0, 0, 0, 1, 1],
     [0, 0, 0, 1, 1]]
adj2 = [[v for v in range(n) if M[u][v] == 1 and v != u] for u in range(n)]
print(adj2)                      # [[1], [0, 2], [1], [4], [3]]

For a directed edge u → v, you would only do adj[u].append(v).

The visited array, and why we cannot live without it

A graph, unlike a tree, has no "down" direction. In an undirected graph, if 1 lists 2 as a neighbour, then 2 also lists 1. Start at 1, go to 2, and 2 says "my neighbour 1 is waiting"… you go back to 1, then to 2 again, forever. A visited array (one True/False slot per node) breaks the loop: mark a node the moment you reach it, and never enter a marked node again.

Connected components

A connected component is a group of nodes where you can walk from any node to any other node using edges, and nothing outside the group is reachable. The picture above has 2 components: {1, 2, 3} and {4, 5}. "Province" in this problem is just another name for a connected component.

The two ways to walk a graph

DFS: Depth First SearchBFS: Breadth First Search
ideaGo deep along one neighbour, then its neighbour, and so on. Come back only when stuck.Visit the start, then all its neighbours, then their neighbours: layer by layer, like a ripple.
toolrecursion (Python's call stack) or an explicit stacka queue, collections.deque
for counting componentsEither one is fine. Both reach every node that is connected to the start, and that is all we need.
the two templates on an adjacency list
from collections import deque

adj = [[1], [0, 2], [1], [4], [3]]
n = len(adj)

def dfs(u, visited, order):
    visited[u] = True                 # mark as soon as we arrive
    order.append(u)
    for v in adj[u]:
        if not visited[v]:
            dfs(v, visited, order)

def bfs(start, visited, order):
    visited[start] = True
    queue = deque([start])
    while queue:
        u = queue.popleft()
        order.append(u)
        for v in adj[u]:
            if not visited[v]:
                visited[v] = True     # mark when we put it in the queue
                queue.append(v)

seen = [False] * n
out = []
dfs(0, seen, out)
print(out)                            # [0, 1, 2]
seen = [False] * n
out = []
bfs(3, seen, out)
print(out)                            # [3, 4]

Notice that one call from node 0 reached only 0, 1, 2. It could not reach 3 or 4. That is exactly the fact this problem is built on.


Part A · Count provinces with DFS

LeetCode 547 · in the sheet it is also called "count number of components"

1The question in simple words

There are n cities. Some pairs have a direct road between them. Connection also passes through a middle city: if A is joined to B, and B is joined to C, then A and C count as connected too (indirectly, through B).

A province is a group of cities that are all connected to each other (directly or indirectly), with no city outside the group connected to them. Return how many provinces there are, which means how many separate groups.

The roads are given as an n × n grid called isConnected: isConnected[i][j] = 1 means city i and city j have a direct road, and 0 means they don't.

the teacher's example (cities 1..5)
(1)—(2)—(3)        (4)—(5)

 group 1: 1, 2, 3     group 2: 4, 5
 answer = 2
its isConnected matrix
        1  2  3  4  5
  1  [  1, 1, 0, 0, 0 ]
  2  [  1, 1, 1, 0, 0 ]
  3  [  0, 1, 1, 0, 0 ]
  4  [  0, 0, 0, 1, 1 ]
  5  [  0, 0, 0, 1, 1 ]

Cities 1 and 3 have no direct road (row 1, column 3 is 0), but both are joined to 2, so they still sit in the same province.

2What the constraints tell us

3Intuition: how to think about it

Picture the cities as islands of houses, and you have a tin of paint. Pick any unpainted city and spill paint on it. The paint flows along every road to every city it can reach, directly or through others, and stops only where roads end. One spill paints one whole province.

Now walk past every city in order. Whenever you find one that is still unpainted, it must belong to a province you haven't touched yet, so spill paint again and add 1 to your count. When you have walked past every city, the number of spills is the number of provinces.

In code, "spilling paint" is one DFS call, and "painted" is the visited array.

4Building the logic from the teacher's example

Step 1: reading the matrix: each row is a list of children

The teacher reads the first row slowly: [1, 1, 0, 0, 0].

So row u is simply the list of children (neighbours) of node u, written as 0/1 flags. Row 2 is [1, 1, 1, 0, 0]: city 2 is joined to 1, to itself, and to 3.

Doubt 1: in other graph problems we first build an adjacency list. Do we need one here?
→ No. The reason we normally build one is to answer "who are the children of u?" quickly. This matrix already answers that: go to row u and walk across it, and every column with a 1 is a child. So the input itself is our adjacency structure (an adjacency matrix), and we skip the building step.
Doubt 2: the teacher talks about city 1, but the code uses row 0. Why "node − 1"?
→ Python lists start at index 0. Her drawing labels cities 1..5, so city k lives in row k − 1. In the code we simply number the cities 0..n−1 from the start, and then node u is row u with no subtraction.

Step 2: one DFS marks one whole group

Send city 1 into a DFS. The DFS has a single job: mark the node as visited, then go to each unvisited child and do the same. From 1 it reaches 2, and from 2 it reaches 3. Then nothing new is left, so the call ends. Visited is now {1, 2, 3}. It could not reach 4 or 5, because no road leads there.

Now make a second call from 4 (the teacher adds that starting from 5 would work just as well, since any unvisited city of that group will do). It marks 4 and then 5. Now every city is visited, after 2 calls. And there are 2 groups. That's no accident: number of fresh DFS calls = number of provinces.

Step 3: why must we mark visited? (the infinite loop)

Without marking: at 1 we go to child 2. Row 2 says 1 is a child of 2, so we go to 1. Row 1 says 2 is a child, so we go to 2… The calls bounce 1 → 2 → 1 → 2 forever (in Python this ends in a RecursionError). Marking 1 as visited before looking at its children stops 2 from going back.

Rule 1Mark a node visited first, before looking at its children.

Step 4: how big is the visited array, and how do we know n?

The matrix has one row per city, so n = len(isConnected). Make visited of size n, all False (the teacher uses an int array with 0/1 in Java; True/False is the same idea). Slot u says whether city u has been painted.

Step 5: which cities do we start a DFS from?

We don't know in advance how far one call will spread. So we try every city in order, but only call DFS if it is still unvisited:

Rule 2Loop over all cities. Only an unvisited city starts a new DFS, and every such start adds 1 to the count.
Doubt 3: why not just call DFS once from city 1?
→ Because one call only spreads through roads. A city in a different province has no road to city 1, so the call can never reach it. The outer loop is what finds those other provinces.

Step 6: inside the DFS: which children can we enter?

In DFS(node), walk across row node with an index i from 0 to n−1. Enter i only if both:

  1. isConnected[node][i] == 1, so there really is a road from node to i, and
  2. visited[i] is False, so i is not painted yet.
Doubt 4: what about the diagonal (city joined to itself)? Do we need an extra i != node check?
→ No. When DFS(node) walks its row, it meets isConnected[node][node] = 1. But we marked node visited as the very first line, so condition 2 fails and we don't go "from 2 to 2". The visited check quietly handles the self-loop. The teacher points this out in her walk-through: at city 1, column 1 is a 1, "but it is already visited, so we won't go".
Doubt 5: the teacher's first draft said != 1. Which is right?
→ She corrected herself on screen: it must be == 1. A 1 means "there is a road", and we want to follow roads. With != 1 you'd be walking to cities you are not connected to, and everything would merge into one group.

5Approach steps

  1. n = len(isConnected), visited = [False] * n, provinces = 0.
  2. For every city i from 0 to n−1: if i is not visited, call dfs(i) and do provinces += 1.
  3. dfs(node): mark visited[node] = True. Then for every i from 0 to n−1: if isConnected[node][i] == 1 and i is not visited, call dfs(i).
  4. Return provinces.

6Code (Python)

Number of Provinces with DFS
class Solution:
    def findCircleNum(self, isConnected):
        n = len(isConnected)              # one row per city
        visited = [False] * n
        provinces = 0

        for i in range(n):
            if not visited[i]:            # a city no earlier call reached
                self.dfs(i, visited, isConnected)
                provinces += 1            # one new call = one new province
        return provinces

    def dfs(self, node, visited, isConnected):
        visited[node] = True              # mark first
        n = len(isConnected)              # n is not known in here, so read it again
        for i in range(n):                # walk across row "node"
            if isConnected[node][i] == 1 and not visited[i]:
                self.dfs(i, visited, isConnected)

The teacher hit an error on screen because her DFS function used n, which only existed in the main function. The fix was to read the length again inside DFS, as in the line marked above.

7Code line by line

linewhat it means
n = len(isConnected)Number of rows = number of cities.
visited = [False] * nNobody is painted yet.
for i in range(n): if not visited[i]:Try every city as a start, but only the unpainted ones. A painted city already belongs to a province we counted.
self.dfs(i, visited, isConnected)Paint the whole province that contains city i. We pass the same visited list, so marks made inside are seen by the outer loop.
provinces += 1Each fresh start found a brand-new group.
visited[node] = TrueMark on arrival, so no child can bounce back to us.
for i in range(n):Look at every column of row node, which means every possible child.
if isConnected[node][i] == 1 and not visited[i]:Go only along a real road, and only to an unpainted city. This also skips the diagonal.
self.dfs(i, visited, isConnected)Go deep: paint i and everything reachable from i before coming back to the next column.

8Dry run

The teacher's example, written with code indexes: cities 1..5 become 0..4. So the roads are 0–1, 1–2 and 3–4.

(0)—(1)—(2)        (3)—(4)
stepwhere we arecolumns checked in this rowcall stack aftervisited afterprovinces
1outer i = 0, unvisited → start dfs(0)-dfs(0){0}0 (+1 when the call returns)
2dfs(0) walks row 0col 0: 1 but visited (self) ✗ · col 1: 1, unvisited ✓dfs(0), dfs(1){0, 1}0
3dfs(1) walks row 1col 0: visited ✗ · col 1: self ✗ · col 2: 1, unvisited ✓dfs(0), dfs(1), dfs(2){0, 1, 2}0
4dfs(2) walks row 2col 0: 0 ✗ · col 1: visited ✗ · col 2: self ✗ · cols 3, 4: 0 ✗dfs(0), dfs(1){0, 1, 2}0
5back in dfs(1), finish row 1cols 3, 4: 0 ✗dfs(0){0, 1, 2}0
6back in dfs(0), finish row 0cols 2, 3, 4: 0 ✗(empty){0, 1, 2}1
7outer i = 1, i = 2already visited → skip(empty){0, 1, 2}1
8outer i = 3, unvisited → start dfs(3)cols 0–2: 0 ✗ · col 3: self ✗ · col 4: 1, unvisited ✓dfs(3), dfs(4){0, 1, 2, 3, 4}1
9dfs(4) walks row 4col 3: visited ✗ · col 4: self ✗(empty after both return)all2
10outer i = 4visited → skip(empty)all2
stack at step 3 (deepest)
dfs(0)dfs(1)dfs(2)
stack at step 8
dfs(3)dfs(4)
after the 1st call (✓ = visited)
(0✓)—(1✓)—(2✓)     (3)—(4)
 provinces = 1
after the 2nd call
(0✓)—(1✓)—(2✓)     (3✓)—(4✓)
 provinces = 2

Two fresh calls were made, so the answer is 2 ✓.

9Complexity & remember

Doubt: the teacher said "200 + 200 = 40,000 = 4 × 10³".
→ Two small slips of the tongue. It should be 200 × 200 = 40,000, which is 4 × 10⁴. The conclusion is the same: far under 10⁸, so it runs fast.
Remember for every node: if not visited → dfs(node), count += 1.
Inside DFS: mark first, then follow every 1 in the row to an unvisited city.
The number of fresh DFS calls is the number of components.

Part B · Count provinces with BFS

The teacher's words: the only real job is to reach every node of a group and mark it visited. DFS does that, but so does BFS. The outer loop and the counting stay exactly the same. Only the "paint one province" function changes.

1The question

Same as Part A: count the groups in isConnected.

2Constraints

Same as Part A. One small bonus for BFS: it uses no recursion, so it never worries about Python's recursion limit, even if n were much bigger.

3Intuition: ripples instead of a deep dive

Instead of chasing one road as far as it goes, BFS paints the start city, then all its direct neighbours, then their neighbours, like a ripple spreading on water. It still stops when no new city can be reached, so one BFS still paints exactly one province.

4What changes, and the "when to mark" question

The teacher's recipe: create a queue and put the start node in it. To find the children, use the same row scan with the same if-check. But where DFS would make a recursive call, BFS just appends that child to the queue, so it gets processed later when it is popped.

Doubt: mark a child visited when we push it, or when we pop it?
→ The teacher says "pop and mark". That still gives the right count, but a city could be pushed twice before it is popped (two neighbours could both see it as unvisited). Example: in a triangle 0–1–2, popping 0 pushes 1 and 2, and popping 1 sees 2 still unmarked and pushes it again. Marking at push time avoids these duplicates, so each city enters the queue once. We use that version. The answer is identical either way.

5Approach steps

  1. Same outer loop: for each unvisited city i, call bfs(i), then provinces += 1.
  2. bfs(start): mark start, put it in a deque.
  3. While the queue isn't empty: pop the front node, then scan its row. For every column i with a 1 that isn't visited: mark it and append it.

6Code (Python)

Number of Provinces with BFS
from collections import deque

class Solution:
    def findCircleNum(self, isConnected):
        n = len(isConnected)
        visited = [False] * n
        provinces = 0
        for i in range(n):
            if not visited[i]:
                self.bfs(i, visited, isConnected)
                provinces += 1
        return provinces

    def bfs(self, start, visited, isConnected):
        n = len(isConnected)
        visited[start] = True
        queue = deque([start])
        while queue:
            node = queue.popleft()
            for i in range(n):
                if isConnected[node][i] == 1 and not visited[i]:
                    visited[i] = True     # mark when pushed: never queued twice
                    queue.append(i)       # instead of a recursive call

7Code line by line (only the new lines)

linewhat it means
visited[start] = True queue = deque([start])Paint the starting city and line it up to have its row scanned.
while queue:Keep going while some painted city still has an unscanned row.
node = queue.popleft()Take the city that has waited longest (front of the queue). This is what makes it level by level.
if isConnected[node][i] == 1 and not visited[i]:Exactly the DFS check: a real road, to an unpainted city.
visited[i] = True queue.append(i)Paint it now, and scan its row later.

8Dry run

Same graph: (0)—(1)—(2) (3)—(4).

stepnode poppednew neighbours foundqueue aftervisited afterprovinces
1outer i = 0 → bfs(0)-[0]{0}0
201[1]{0, 1}0
312 (0 is visited)[2]{0, 1, 2}0
42none[ ]{0, 1, 2}1
5outer i = 1, 2 visited → skip; i = 3 → bfs(3)-[3]{0..3}1
634[4]all1
74none[ ]all2
bfs(0)0pop 0 → push 1
1pop 1 → push 2
2pop 2 → nothing new → queue empty, province 1 done
bfs(3)3pop 3 → push 4
4pop 4 → nothing new → province 2 done

9Complexity & remember

RememberBFS = DFS with "recursive call" replaced by "mark + append to a deque". Same outer loop, same count. The teacher's tip: practise this question both ways.

Part C · Revision page

DFS (recursion)BFS (queue)
outer loopfor each city: if not visited → traverse from it, count += 1
children of uevery column i in row u with isConnected[u][i] == 1
moving to a childrecursive call dfs(i)visited[i] = True; queue.append(i)
where pending work livesthe call stackthe deque
time / spaceO(n²) / O(n)O(n²) / O(n)
input formatwhat you do first
adjacency matrix (this problem)nothing: row u already lists the children of u
edge listbuild adj with two appends per undirected edge
adjacency listnothing: use adj[u]
If you remember only 5 lines 1. A province is a connected component: a group you can walk around in using roads.
2. The matrix row of u is u's list of children, so no adjacency list is needed.
3. One DFS/BFS from a city marks its whole province as visited.
4. Loop over all cities, and every unvisited one starts a new traversal and adds 1.
5. O(n²) time because each row is scanned once. O(n) space.
Mistakes to avoid ✗ forgetting visited → 1 → 2 → 1 → 2 … forever
✗ writing != 1 instead of == 1 for "there is a road"
✗ counting inside DFS instead of in the outer loop (you'd count cities, not groups)
✗ using n inside the helper when it only exists in the main function
✗ thinking the diagonal 1s are roads to other cities (visited makes them harmless)
test it yourself (paste under either solution above)
s = Solution()
print(s.findCircleNum([[1,1,0,0,0],[1,1,1,0,0],[0,1,1,0,0],[0,0,0,1,1],[0,0,0,1,1]]))  # 2
print(s.findCircleNum([[1,1,0],[1,1,0],[0,0,1]]))   # 2
print(s.findCircleNum([[1,0,0],[0,1,0],[0,0,1]]))   # 3
print(s.findCircleNum([[1,1,1],[1,1,1],[1,1,1]]))   # 1
print(s.findCircleNum([[1]]))                       # 1

Based on this video: Number of Provinces | Graph