DSA sheet · Graphs · Connected components on a grid

Number of Islands

This is Number of Provinces again, but now the graph is hidden inside a grid. Every land cell is a node, and two land cells touching up/down/left/right are joined by an edge. The teacher's point is that counting islands = counting connected components. She solves it with BFS this time (Provinces was DFS), shows the dr/dc direction trick, and shows how to save memory by marking visited inside the grid itself. This grid-walking template is used in almost every grid problem that follows.

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 and grids as graphs

Nodes, edges, and the kinds of graphs

A graph is a set of nodes (points) joined by edges (lines). An edge can be undirected (usable both ways, drawn (a)—(b)) or directed (one way, drawn (a)→(b)). It can also carry a number, a weight (drawn (a)—7—(b)). This problem's graph is undirected and unweighted.

Input forms and the adjacency list

Normal graph problems give an edge list ([[0,1],[1,2]]), an adjacency matrix (n × n of 0/1) or an adjacency list (adj[u] = neighbours of u). Usually we turn the input into an adjacency list first:

edge list → adjacency list
n = 4
edges = [[0, 1], [1, 2], [2, 3]]
adj = [[] for _ in range(n)]
for u, v in edges:
    adj[u].append(v)
    adj[v].append(u)      # leave this line out for a directed graph
print(adj)                # [[1], [0, 2], [1, 3], [2]]

Visited and connected components

In a graph you can walk in circles (a → b → a …). So every traversal keeps a visited record: mark a node when you reach it, and never enter a marked node again. A connected component is a group of nodes that can all reach each other, with no edge leaving the group. "Start a traversal from every unvisited node and count the starts" counts the components. That was the whole idea of Number of Provinces.

BFS and DFS

BFS (breadth first search) uses a queue: take the oldest node, add its unvisited neighbours to the back. It spreads out in rings, layer by layer. DFS (depth first search) uses recursion or a stack: dive into one neighbour as deep as possible, then come back. For counting components either works, because both reach every node connected to the start.

Grids as graphs

A grid with m rows and n columns is a graph in disguise:

            (r-1, c)
               ↑
 (r, c-1)  ←  (r, c)  →  (r, c+1)
               ↓
            (r+1, c)

The dr/dc trick. Instead of writing 4 near-identical blocks, store the 4 moves as two small lists: dr (change in row) and dc (change in column). Pair number i is (dr[i], dc[i]), and the neighbour is (r + dr[i], c + dc[i]).

Bounds check. A neighbour of an edge cell can fall outside the grid (row −1, or column n). Always check 0 <= nr < m and 0 <= nc < n before reading grid[nr][nc]. In Python, grid[-1] doesn't even crash: it silently reads the last row, which gives wrong answers. So this check is essential.

Marking visited in place. Instead of a separate visited grid, you can overwrite a cell (for example, land → water) once you've reached it. We'll see exactly why that's safe in Part A.

the 4 neighbours of a cell, with the bounds check
grid = ["110",
        "011"]
m, n = len(grid), len(grid[0])
dr = [-1, 0, 1, 0]       # up, left, down, right
dc = [0, -1, 0, 1]
r, c = 0, 1
for i in range(4):
    nr, nc = r + dr[i], c + dc[i]
    if 0 <= nr < m and 0 <= nc < n:
        print((nr, nc), grid[nr][nc])
# prints (0, 0) 1, then (1, 1) 1, then (0, 2) 0   (up is outside, so skipped)

Part A · Count islands with BFS

LeetCode 200

1The question in simple words

You get an m × n grid of '1' (land) and '0' (water). An island is a group of land cells joined horizontally or vertically (left, right, up, down). Diagonal touching does not join cells. Everything outside the grid counts as water. Return how many islands there are.

the teacher's grid
     c: 0 1 2 3 4
 r=0    1 1 0 0 0
 r=1    1 1 0 0 0
 r=2    0 0 1 0 0
 r=3    0 0 0 1 1
its islands
        A A . . .
        A A . . .
        . . B . .
        . . . C C
   3 islands: A, B, C

Notice that B and C touch only at a corner (diagonally): (2,2) and (3,3). That does not join them, so they are 2 separate islands.

2What the constraints tell us

3Intuition: how to think about it

Walk over the map row by row. The moment your foot lands on a piece of land you haven't explored yet, you've found a new island, so count it. Then, before moving on, explore that whole island and mark every one of its cells, so you'll never count it again when your scan reaches those cells later.

The teacher's observation: this is word-for-word the Number of Provinces idea. Provinces asked for groups of connected cities. Here we want groups of connected land cells. Same pattern: outer loop over all nodes + one traversal per unvisited node + count the traversals.

4Building the logic from the teacher's example

Step 1: where can we move from a land cell?

Start at (0,0). Up and left are outside the grid. Down (1,0) is land, and right (0,1) is land. From there we also reach (1,1). The island stops wherever there is a 0, because water can never be part of an island. And we never step diagonally, because the question only allows horizontal and vertical joins.

Rule 1: a valid neighbourIt is one of the 4 directions, it is inside the grid, and it is land ('1').

Step 2: start only from land

A traversal from a water cell would explore nothing, and it is not an island. So the outer loop starts a BFS only on a land cell that hasn't been visited. Every such start is a new island → count it.

Rule 2Loop over every cell (two loops: rows, then columns). If the cell is unvisited land → BFS from it, islands += 1.
Doubt 1: why loop over every cell? Can't we pick the starting cells smartly?
→ We don't know in advance which cells are the "first cell" of an island, or how far one BFS will spread. Scanning every cell, and skipping those already explored, is the simple way to make sure no island is missed and none is counted twice.

Step 3: why mark visited at all?

(0,0)'s child is (1,0). But (1,0)'s children include (0,0) again! Without marking, we would push (0,0) again, then (1,0) again… an endless loop. Marking a cell as soon as we reach it stops this.

Step 4: the "visited inside the grid" trick (the 10% case)

The teacher says that in most graph problems (about 90%) you need a separate visited array, but sometimes (the other 10%) the input itself can remember what you visited. Her reasoning goes like this:

This saves a whole m × n visited grid. But it changes the input. The teacher's advice: do it only if the interviewer allows it, or else keep a separate visited grid (O(m·n) extra space).

Doubt 2: once a cell becomes '0', how does the outer loop still know it's land we've already counted?
→ It doesn't need to know. The outer loop only cares about unexplored land. A cell turned to '0' is either real water or already-counted land, and in both cases we must not start a BFS there. So "grid[i][j] == '1'" means exactly "land that no BFS has reached yet".

Step 5: BFS: mark when you push, not when you pop

BFS starts by putting (r, c) in a queue and immediately marking it '0'. Then, while the queue isn't empty: pop a cell, try its 4 directions, and for each valid land neighbour, push it and mark it '0' right away. The teacher's reason: the start cell was marked at the moment it entered the queue, so follow the same rule for every cell. Marking on push means a cell can never be pushed twice by two different neighbours.

Doubt 3: after pushing (1,0), why don't we jump to (1,0) and explore it next, like DFS would?
→ That's the difference between BFS and DFS. In BFS, the cell we popped (here (0,0)) stays "the parent" until all 4 of its directions are tried. So we also push (0,1) before touching (1,0)'s children. The children wait in the queue in order. That gives the ring-by-ring spread.

Step 6: the direction order the teacher uses

dr = [-1, 0, 1, 0], dc = [0, -1, 0, 1]. Read them as pairs: (−1, 0) = up, (0, −1) = left, (1, 0) = down, (0, 1) = right. The teacher mentions the alternative is to write 4 separate calls/blocks with r-1, c / r+1, c / r, c-1 / r, c+1. The arrays just let one small loop do all four. The order doesn't change the answer, only the visiting order.

5Approach steps

  1. m = len(grid), n = len(grid[0]), islands = 0.
  2. For every cell (i, j): if grid[i][j] == '1' → bfs(i, j), then islands += 1.
  3. bfs(r, c): push (r, c), set it to '0'.
  4. While the queue isn't empty: pop (r, c). For each of the 4 directions compute (nr, nc). If it is inside the grid and equal to '1': push it and set it to '0'.
  5. Return islands.

6Code (Python)

Number of Islands with BFS (marks visited inside the grid)
from collections import deque

class Solution:
    def numIslands(self, grid):
        m, n = len(grid), len(grid[0])
        islands = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '1':          # unexplored land = a new island
                    self.bfs(grid, i, j)
                    islands += 1
        return islands

    def bfs(self, grid, r, c):
        m, n = len(grid), len(grid[0])         # sizes again (not known in here)
        dr = [-1, 0, 1, 0]                     # up, left, down, right
        dc = [0, -1, 0, 1]
        queue = deque()
        queue.append((r, c))
        grid[r][c] = '0'                       # visited = turned into water
        while queue:
            r, c = queue.popleft()
            for i in range(4):
                nr, nc = r + dr[i], c + dc[i]
                if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == '1':
                    queue.append((nr, nc))
                    grid[nr][nc] = '0'         # mark when pushed

LeetCode passes grid as a list of lists of characters, so grid[r][c] = '0' works. (A list of plain strings can't be changed in place. The test snippet below converts strings to lists first.)

7Code line by line

linewhat it means
m, n = len(grid), len(grid[0])Number of rows and columns.
for i in range(m): for j in range(n):Visit every cell once in the outer scan.
if grid[i][j] == '1':Still '1' means land that no earlier BFS reached, so this is the first cell of a new island.
self.bfs(grid, i, j) islands += 1Sink this whole island, and count it once.
dr = [-1, 0, 1, 0] dc = [0, -1, 0, 1]The 4 moves as (row change, column change) pairs.
queue.append((r, c)) grid[r][c] = '0'Start the queue with the first cell and mark it at once.
r, c = queue.popleft()Take the oldest waiting cell. Its row and column come out as a pair.
nr, nc = r + dr[i], c + dc[i]The neighbour in direction i.
0 <= nr < m and 0 <= nc < nInside the grid. This is checked first, so the next part never reads outside.
and grid[nr][nc] == '1'Land, and not explored yet (explored land is now '0').
queue.append((nr, nc)) grid[nr][nc] = '0'It joins this island: queue it and mark it right away, so nobody pushes it again.

8Dry run on the teacher's grid

Directions are tried in the order up, left, down, right. "✗out" = outside the grid, "✗0" = water or already marked.

stepcell poppedneighbours checked (up, left, down, right)queue aftercells marked so farislands
1scan hits (0,0) = '1' → bfspush (0,0), mark it[(0,0)](0,0)0
2(0,0)✗out · ✗out · (1,0) ✓ · (0,1) ✓[(1,0), (0,1)]+ (1,0), (0,1)0
3(1,0)(0,0) ✗0 · ✗out · (2,0) ✗0 · (1,1) ✓[(0,1), (1,1)]+ (1,1)0
4(0,1)✗out · (0,0) ✗0 · (1,1) ✗0 (already marked!) · (0,2) ✗0[(1,1)]same0
5(1,1)(0,1) ✗0 · (1,0) ✗0 · (2,1) ✗0 · (1,2) ✗0[ ]same1
6scan (0,1)…(2,1): all '0' now → skip; (2,2) = '1' → bfspush (2,2), mark it[(2,2)]+ (2,2)1
7(2,2)all 4 are '0'[ ]same2
8scan … (3,3) = '1' → bfspush (3,3), mark it[(3,3)]+ (3,3)2
9(3,3)(2,3) ✗0 · (3,2) ✗0 · ✗out · (3,4) ✓[(3,4)]+ (3,4)2
10(3,4)(2,4) ✗0 · (3,3) ✗0 · ✗out · ✗out[ ]all land3
11scan reaches (3,4): '0' → skip. End.3

Step 4 shows why marking on push matters: (1,1) was pushed by (1,0) and is already '0', so (0,1) does not push it a second time.

after BFS #1 (x = sunk)
x x 0 0 0
x x 0 0 0
0 0 1 0 0
0 0 0 1 1     islands = 1
after BFS #2
x x 0 0 0
x x 0 0 0
0 0 x 0 0
0 0 0 1 1     islands = 2
after BFS #3
x x 0 0 0
x x 0 0 0
0 0 x 0 0
0 0 0 x x     islands = 3
BFS #10,0pop (0,0) → push (1,0), (0,1)
1,00,1pop (1,0) → push (1,1)
0,11,1pop (0,1) → nothing new
1,1pop (1,1) → nothing new → queue empty, back to the scan

9Complexity & remember

Remember Islands = connected components on a grid.
Scan all cells → unexplored land → BFS + count.
Neighbour test: in bounds first, then == '1'. Mark '0' when pushing.

Part B · Count islands with DFS

The teacher asks us to submit this question both ways, so that "reaching a node's children" becomes second nature. With DFS, everything is the same except how one island is explored.

1The question

Same as Part A.

2Constraints, and a Python warning

Same constraints. But a recursive DFS on a 300 × 300 grid of all land can go 90,000 calls deep. Python stops at about 1000 by default (RecursionError). Two fixes: raise the limit with sys.setrecursionlimit (fine on LeetCode, risky elsewhere), or use an explicit stack, which has no depth limit. Both are shown below.

3Intuition

From a land cell, sink it, then dive into its first land neighbour, then that one's first land neighbour, and so on. Only when you're stuck do you come back and try the next direction. It's the same island, sunk in a different order.

4What changes from BFS

Doubt: in the recursive version, why is the bounds check at the start of the call instead of before calling?
→ Either place works. Checking at the top means one check covers all 4 calls, and it doubles as the base case that stops the recursion: outside the grid or water → return immediately.

5Approach steps

  1. Same outer scan and count as Part A.
  2. dfs(r, c): if outside the grid or not '1' → return.
  3. Set grid[r][c] = '0', then call dfs on up, down, left, right.

6Code (Python)

Number of Islands with recursive DFS
import sys

class Solution:
    def numIslands(self, grid):
        sys.setrecursionlimit(10**6)           # deep islands need deep recursion
        m, n = len(grid), len(grid[0])
        islands = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '1':
                    self.dfs(grid, i, j)
                    islands += 1
        return islands

    def dfs(self, grid, r, c):
        if r < 0 or r >= len(grid) or c < 0 or c >= len(grid[0]):
            return                             # stepped outside
        if grid[r][c] != '1':
            return                             # water or already sunk
        grid[r][c] = '0'                       # mark first
        self.dfs(grid, r - 1, c)               # up
        self.dfs(grid, r + 1, c)               # down
        self.dfs(grid, r, c - 1)               # left
        self.dfs(grid, r, c + 1)               # right
Number of Islands with an explicit stack (no recursion limit)
class Solution:
    def numIslands(self, grid):
        m, n = len(grid), len(grid[0])
        dr = [-1, 0, 1, 0]
        dc = [0, -1, 0, 1]
        islands = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] != '1':
                    continue
                islands += 1
                stack = [(i, j)]
                grid[i][j] = '0'
                while stack:
                    r, c = stack.pop()             # newest first = depth first
                    for d in range(4):
                        nr, nc = r + dr[d], c + dc[d]
                        if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == '1':
                            grid[nr][nc] = '0'
                            stack.append((nr, nc))
        return islands

7Code line by line (recursive version)

linewhat it means
sys.setrecursionlimit(10**6)Allow very deep recursion for big all-land islands.
if r < 0 or r >= len(grid) or ...: returnBase case 1: we stepped off the map, which counts as water.
if grid[r][c] != '1': returnBase case 2: water, or land we already sank.
grid[r][c] = '0'Sink it before the 4 calls, so a neighbour can't come back to it.
self.dfs(grid, r - 1, c) … (r, c + 1)The teacher's "4 calls": try every direction. Each one returns at once if it's not land.

8Dry run: island A with recursive DFS

  1. dfs(0,0): land → sink. Call up (−1,0): outside → return.
  2. Call down dfs(1,0): land → sink. Its up (0,0) is now '0' → return. Its down (2,0) is water → return. Its left is outside. Its right dfs(1,1): land → sink.
  3. Inside dfs(1,1): up dfs(0,1): land → sink. All of (0,1)'s neighbours are '0' or outside → it returns.
  4. dfs(1,1)'s other directions are water/sunk → return. Back in (1,0) → return. Back in (0,0): left is outside, and right (0,1) is already '0' → return.
  5. Island A is fully sunk with one top-level call → islands = 1. B and C go the same way → 3.
deepest point for island A
dfs(0,0)dfs(1,0)dfs(1,1)dfs(0,1)

9Complexity & remember

RememberBFS and DFS give the same count, and both are O(m·n). The teacher's point: here it's not about speed, only about reaching every land cell once, so pick whichever you write more comfortably. In Python, prefer BFS or an explicit stack for big grids.

Part C · Revision page

Number of ProvincesNumber of Islands
nodea city (row of the matrix)a land cell (r, c)
neighbourscolumns with 1 in that rowup/left/down/right, in bounds, '1'
visiteda visited arrayoverwrite '1' → '0' (or a visited grid)
answernumber of traversals started from an unvisited node
timeO(n²)O(m·n)
BFS (teacher's code)DFS recursiveDFS explicit stack
containerdeque, popleft()call stacklist, pop()
where the check isbefore pushingat the top of the callbefore pushing
deep island in Pythonfineneeds a higher recursion limitfine
If you remember only 5 lines 1. A grid is a graph: cells are nodes, the 4 side-neighbours are edges.
2. dr = [-1,0,1,0], dc = [0,-1,0,1] → neighbour (r+dr[i], c+dc[i]).
3. Check 0 <= nr < m and 0 <= nc < n before reading the cell.
4. Mark visited by turning '1' into '0', the moment you push.
5. Every BFS/DFS started by the outer scan = one island.
Mistakes to avoid ✗ comparing with the number 1 instead of the character '1'
✗ reading grid[nr][nc] before the bounds check (and grid[-1] silently wraps around in Python)
✗ moving diagonally: only 4 directions join land
✗ marking when popping, so the same cell gets queued many times
✗ counting inside the BFS instead of once per start
✗ deep recursion on a 300 × 300 grid without raising the limit
test it yourself (paste under any solution above)
def make(rows):
    return [list(row) for row in rows]     # LeetCode gives lists of characters

s = Solution()
print(s.numIslands(make(["11000", "11000", "00100", "00011"])))   # 3
print(s.numIslands(make(["11110", "11010", "11000", "00000"])))   # 1
print(s.numIslands(make(["000", "000"])))                         # 0
print(s.numIslands(make(["1"])))                                  # 1
print(s.numIslands(make(["101", "010", "101"])))                  # 5

Based on this video: Number of Islands | Graph