Grid Traversal Islands and Flood Fill
A grid is a graph you never have to build. Each cell is a node; its edges are
the neighbours you can step to. That single reframing turns a large family of
matrix problems into ordinary DFS or
BFS — no adjacency list required, because
(r+1, c), (r-1, c), (r, c+1), (r, c-1) is the adjacency function.
What remains is bookkeeping: staying in bounds, not revisiting cells, and knowing which of the two traversals the question wants.
What you’ll learn
Section titled “What you’ll learn”- The four-direction template, and why
sink(mutate in place) usually beats avisitedset. - Counting connected components — the outer loop that makes island counting work.
- DFS vs BFS on grids: identical for counting, different for shortest paths and for stack depth.
- The boundary-inversion trick — the idea behind LC 130, 1020 and 417, and the most transferable insight here.
- Three real LeetCode problems solved in the browser: 200, 695, 733.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”A grid is a graph in disguise — nodes are cells, edges are adjacency. Counting islands is therefore connected components, an algorithm from earlier in this phase wearing a 2-D costume:
The flood is what keeps the outer double loop linear rather than quadratic: every cell is claimed exactly once across the whole run, no matter how many times the scan passes over it.
Cells are marked visited on ENQUEUE, not on dequeue. Marking on dequeue lets the same cell enter the queue several times, which is the standard way this solution degrades from linear to quadratic.
The template
Section titled “The template”def count_regions(grid):
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
def sink(r, c):
# bounds check and "is it land" check, both in the base case
if not (0 <= r < rows and 0 <= c < cols) or grid[r][c] != 1:
return
grid[r][c] = 0 # mark visited by mutating
sink(r + 1, c)
sink(r - 1, c)
sink(r, c + 1)
sink(r, c - 1)
regions = 0
for r in range(rows): # the outer scan finds each region
for c in range(cols):
if grid[r][c] == 1:
regions += 1 # a new region begins here
sink(r, c) # then erase all of it
return regionsTwo structural pieces to internalise:
- The base case does both checks. Bounds first, then “is this a cell I care about”. Testing bounds at the call site instead means four copies of the same condition.
- The outer double loop is the component counter.
sinkerases one entire region, so the number of times the outer loop starts a sink is the number of regions. This is exactly connected-component counting from Graph Traversal.
DFS or BFS?
Section titled “DFS or BFS?”| DFS | BFS | |
|---|---|---|
| Count / measure regions | ✅ equally good | ✅ equally good |
| Shortest path in a grid | ❌ wrong | ✅ correct |
| Space | worst-case recursion | -ish frontier |
| Risk | Python RecursionError on a large solid grid | none |
For counting they are interchangeable — pick either. For shortest path only BFS works, because BFS visits in non-decreasing distance order.
Direction vectors
Section titled “Direction vectors”For anything beyond four directions, a vector list beats four hard-coded calls:
DIRS_4 = ((1, 0), (-1, 0), (0, 1), (0, -1))
DIRS_8 = DIRS_4 + ((1, 1), (1, -1), (-1, 1), (-1, -1))
for dr, dc in DIRS_4:
nr, nc = r + dr, c + dc
...Eight-direction connectivity (LC 1254 and many “count blobs” variants) is then a one-token change rather than four more recursive calls.
The boundary-inversion trick
Section titled “The boundary-inversion trick”This is the idea worth carrying away from this page. Several problems ask about regions that do not touch the border — captured regions, enclaves. Searching for “regions with no border cell” directly is awkward: you would have to explore a region fully, then decide.
Invert it. Flood inward from the border to mark everything that is connected to the edge. Whatever remains unmarked is, by definition, enclosed.
def solve(board):
"""LC 130: flip every 'O' region NOT touching the border to 'X'."""
if not board or not board[0]:
return board
rows, cols = len(board), len(board[0])
def mark_safe(r, c):
if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != "O":
return
board[r][c] = "S" # temporarily: reachable from edge
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
mark_safe(r + dr, c + dc)
for r in range(rows): # start ONLY from the border
mark_safe(r, 0)
mark_safe(r, cols - 1)
for c in range(cols):
mark_safe(0, c)
mark_safe(rows - 1, c)
for r in range(rows): # then a single rewrite pass
for c in range(cols):
board[r][c] = "O" if board[r][c] == "S" else "X"
return boardOnce you see this, LC 130 (capture regions), LC 1020 (count enclaves) and LC 417 (Pacific-Atlantic water flow) are all the same three-step recipe: flood from the border, then read off what was or was not reached. LC 417 runs it twice — once from each ocean — and intersects the results.
| Time | Space | |
|---|---|---|
| Any single grid traversal | worst case for the stack/queue |
Complexity
Section titled “Complexity”Let the grid be , so cells.
| Quantity | Cost | Why |
|---|---|---|
| Time | each cell is sunk at most once, and each sink looks at 4 neighbours | |
| Space, sinking the grid | recursion, extra data | the call stack is the cost, and it is invisible in the source |
| Space, BFS + queue | typical, worst | the queue holds one frontier |
Space, visited set instead of sinking | buys you an unmutated input |
The outer double loop does not multiply the cost. It visits cells and starts a sink only on cells that are still land, and each sink permanently removes the cells it touches — so across the whole run every cell is entered once. Counting the outer loop and the sinks separately, and then adding rather than multiplying them, is the argument to give out loud.
Dry run
Section titled “Dry run”grid = [[1,1,0],[1,0,1],[0,0,1]], the template exactly as written. The outer scan
goes row by row; sink recurses down, up, right, left in that order.
| # | outer loop at | action | grid after |
|---|---|---|---|
| 1 | (0,0) is land | regions = 1, start sink | — |
| 2 | sink (0,0) → mark 0, recurse down | [[0,1,0],[1,0,1],[0,0,1]] | |
| 3 | sink (1,0) → mark 0; its 4 neighbours are (2,0)=0, (0,0)=already sunk, (1,1)=0, out of bounds — all return | [[0,1,0],[0,0,1],[0,0,1]] | |
| 4 | back at (0,0): up is out of bounds; right → sink (0,1) → mark 0; its neighbours are (1,1)=0, oob, (0,2)=0, (0,0) sunk | [[0,0,0],[0,0,1],[0,0,1]] | |
| 5 | (0,1), (0,2), (1,0), (1,1) | all now 0 — skipped, no sink started | — |
| 6 | (1,2) is land | regions = 2, start sink | — |
| 7 | sink (1,2) → mark 0, down → sink (2,2) → mark 0 | [[0,0,0],[0,0,0],[0,0,0]] | |
| 8 | (2,0), (2,1), (2,2) | all 0 — nothing starts | — |
Answer 2.
What the trace shows that the code does not:
regionscounts sink starts, not cells. The outer loop passes over five already-sunk land cells (step 5) without incrementing anything, because sinking erased them. That is the entire component-counting mechanism, and it is why no separatevisitedbookkeeping is needed.- Every
sinkcall that does real work is guarded by the same two-part base case. Step 3 makes four recursive calls and all four return immediately — one for bounds, three for “not land”. Roughly 4 calls per cell do nothing, which is why the constant factor is 5× the cell count and still . - The recursion is 3 deep here and deep in the worst case. Follow step 2 → 3:
sink(0,0)is still on the stack whilesink(1,0)runs. On a spiral-shaped island of 40,000 cells that stack is 40,000 frames tall — the concrete reason the ” space” claim fails.
The variant map
Section titled “The variant map”| Variant | What changes | Canonical problem |
|---|---|---|
| Count regions | Count outer-loop starts | 200 Number of Islands |
| Measure regions | sink returns a size; take the max | 695 Max Area of Island |
| Recolour one region | Start from a given cell only | 733 Flood Fill |
| Border inversion | Flood from the border, then invert | 130 · 1020 |
| Two-source reachability | Flood from each source, intersect | 417 Pacific Atlantic |
| Shortest path | BFS, not DFS | 1091 · Multi-source BFS |
| 8-direction connectivity | DIRS_8 instead of DIRS_4 | 1254 and blob-counting variants |
| Count perimeter, not area | Add 4 per land cell, subtract 2 per shared edge | 463 Island Perimeter |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”LC 200 — Number of Islands · Medium
Section titled “LC 200 — Number of Islands · Medium”Problem. Given an m x n binary grid of '1' (land) and '0' (water),
return the number of islands. An island is land connected horizontally or
vertically, surrounded by water.
Constraints. 1 <= m, n <= 300, cells are the characters '0' or
'1'.
Examples. A grid with one large connected landmass gives 1; a grid with
three separate landmasses gives 3; all water gives 0.
Editorial — approach, complexity, follow-ups
This is connected-component counting on an implicit graph. The outer loop finds an unvisited land cell — necessarily a new island, since any previously seen island has been erased — and the sink removes the whole component.
Time : each cell is examined a constant number of times. Space worst case for the stack.
Two practical notes:
- String cells. LeetCode gives
"1"and"0"as characters here (unlike LC 695, which uses integers). Comparing against1silently finds nothing and returns0. It is an irritating inconsistency and a real source of wasted submissions. - Iterative over recursive. At
300 x 300a solid grid is 90,000 cells in one component. Recursive DFS raisesRecursionError; the explicit stack does not.
Follow-ups you should expect:
- “Don’t mutate the input.” Use a
visitedset of(r, c)at space. - “8-directional islands?” Swap in
DIRS_8. - “The grid is a stream / too large for memory.” Process row by row with union-find, merging components between adjacent rows — this is the real-world answer and a common senior follow-up.
- “Islands II — land added one cell at a time (LC 305)?” Union-find with a running component count, since each addition can merge up to four components.
- “Count distinct island shapes (LC 694)?” Normalise each island’s cell offsets relative to its starting cell and hash the resulting tuple.
LC 695 — Max Area of Island · Medium
Section titled “LC 695 — Max Area of Island · Medium”Problem. Given an m x n binary grid of 0 and 1 integers, return
the area (number of cells) of the largest island. If there is no island,
return 0.
Constraints. 1 <= m, n <= 50, cells are the integers 0 or 1.
Examples. The standard 8×13 example gives 6 · [[0,0],[0,0]] gives 0
· [[1]] gives 1
Editorial — approach, complexity, follow-ups
Identical structure to LC 200, except the recursion returns a count instead
of nothing. Because area zeroes each cell as it enters, no cell contributes
twice, and the returned sum is exactly the island’s size.
Time . Space recursion worst case (fine
here at 50 x 50, unlike LC 200’s 300 x 300).
Two details:
- Mark before recursing. If you set
grid[r][c] = 0after the four recursive calls, a neighbour immediately recurses back into this cell and you double-count — or recurse forever. The ordering is load-bearing. default=0onmax. With no land at all, the generator yields only zeros so it happens to work here — but on a grid with no cells at allmaxof an empty sequence raisesValueError.default=0is the safe habit.
Recursion is acceptable at these constraints, which is why this solution reads more cleanly than LC 200’s iterative one. Choosing per-problem based on the stated bounds — rather than always doing one or the other — is the judgement being tested.
Follow-ups you should expect: “Count the perimeter instead (LC 463)?” —
add 4 per land cell and subtract 2 for each shared edge; no traversal needed
at all. “Return the largest island’s cells?” — collect coordinates instead of
counting. “Largest island after flipping one 0 to 1 (LC 827)?” — label
each island with an id and its size, then for every water cell sum the sizes of
its distinct neighbouring island ids plus one.
LC 733 — Flood Fill · Easy
Section titled “LC 733 — Flood Fill · Easy”Problem. Given an image grid, a starting pixel (sr, sc) and a
color, recolour the starting pixel and every pixel connected to it
(4-directionally) that shares the starting pixel’s original colour.
Constraints. 1 <= m, n <= 50, 0 <= pixel values, color < 2^16.
Examples. [[1,1,1],[1,1,0],[1,0,1]], sr=1, sc=1, color=2 gives
[[2,2,2],[2,2,0],[2,0,1]] · filling with the colour it already has leaves
the image unchanged
Editorial — approach, complexity, follow-ups
A single traversal from one starting cell. Recolouring doubles as the visited
mark: once a cell is color it no longer matches start, so the guard stops
re-entry.
Time . Space recursion worst case.
Note also that the visited-marking and the goal-writing are the same operation here, which is unusual and pleasant. In LC 200 you overwrite with a sentinel you do not care about; here the overwrite is the answer.
Follow-ups you should expect: “Iteratively?” — an explicit stack, same
guard. “8-directional?” — DIRS_8. “What if color should replace only cells
matching a given colour rather than the start’s?” — pass it in; the guard
becomes a comparison against that value. “Fill with a gradient / bounded
distance?” — switch to BFS so you know each cell’s distance from the source.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 463Island PerimetereasyNo traversal needed: 4 per land cell, minus 2 per shared edge
- 733Flood FilleasyThe `start == color` guard, or infinite recursion
- 200Number of IslandsmediumComponent counting; cells are **strings** here
- 130Surrounded RegionsmediumBoundary inversion -- flood from the border, then rewrite
- 417Pacific Atlantic Water FlowmediumFlood **uphill** from each ocean, then intersect
- 695Max Area of IslandmediumThe sink returns a size; mark before recursing
- 1020Number of EnclavesmediumBoundary inversion, then count what survives
- 329Longest Increasing Path in a Matrixhard
- 1368Minimum Cost to Make at Least One Valid Path in a Gridhard
- 2290Minimum Obstacle Removal to Reach Cornerhard
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “DFS or BFS?” | Deliberate choice | Interchangeable for counting; BFS is required for shortest path |
| “Do you mutate the input?” | Care about side effects | Sinking is space but destructive; a visited set is and clean. State the trade |
| “What about a huge solid grid?” | Practical Python | Recursive DFS can exceed the 1000-frame limit; use an explicit stack |
| “Regions not touching the border?” | The transferable trick | Flood from the border and invert — do not test regions one at a time |
| “The grid doesn’t fit in memory” | Real-world thinking | Stream row by row with union-find, merging components across adjacent rows |
| “Land is added incrementally” | Choosing the right structure | Union-find with a running component count (LC 305) |
| “8 directions instead of 4?” | Generalisation | A DIRS vector list makes it a one-line change |
Edge-case checklist
Section titled “Edge-case checklist”- Empty grid —
[]or[[]]; guard before readinggrid[0]. - 1×1 grid — both
[[1]](one island, area 1) and[[0]](zero). - All land — one island covering everything; the recursion-depth case.
- All water —
0islands,0area;max(..., default=0). - Start colour equals target colour (LC 733) — must return immediately.
- Islands touching the border — fine for 200/695, the whole point for 130/1020.
- Diagonally adjacent land — not connected under 4-directional rules; a frequent misreading.
- String vs integer cells — LC 200 uses
"1", LC 695 uses1. Check before comparing. - Single row or single column — exercises bounds checks in one dimension.
Self-check
Section titled “Self-check”-
The outer double loop visits every cell and can start a sink at each one. Why is the total still O(rc) rather than O((rc)²)?
In the dry run the outer loop passes over five already-sunk cells without doing anything. Being able to say 'these costs add' is the difference between guessing the complexity and deriving it.
pch.quizShowAnswer
B — Because a sink permanently erases the cells it touches, so across the whole run each cell is entered once — the outer scan and the sinks add, they do not multiply — In the dry run the outer loop passes over five already-sunk cells without doing anything. Being able to say 'these costs add' is the difference between guessing the complexity and deriving it.
-
Is the recursive sinking solution O(1) space?
The extra *data* is O(1); the call stack is not, and it is invisible in the source. A 200×200 all-land grid is one region 40,000 cells long. The safe answers are an explicit stack or BFS.
pch.quizShowAnswer
B — No — the DFS recursion is O(rc) deep in the worst case, and on a large solid grid CPython raises RecursionError past about 1000 frames — The extra *data* is O(1); the call stack is not, and it is invisible in the source. A 200×200 all-land grid is one region 40,000 cells long. The safe answers are an explicit stack or BFS.
-
Where do the bounds check and the 'is this land' check belong?
Guarding at the call site means four copies of the same condition per recursion — four chances to typo one of them. One base case doing both is the shape to memorise.
pch.quizShowAnswer
B — Both in the base case of the recursive function, so there is exactly one copy of each — Guarding at the call site means four copies of the same condition per recursion — four chances to typo one of them. One base case doing both is the shape to memorise.
-
You mark a cell visited *after* recursing into its neighbours instead of before. What breaks?
Mark-then-recurse is the invariant. It is the grid version of 'mark on enqueue, not on dequeue' from the BFS pages.
pch.quizShowAnswer
B — Neighbours recurse back into the unmarked cell, so cells are counted twice and the recursion can revisit forever — Mark-then-recurse is the invariant. It is the grid version of 'mark on enqueue, not on dequeue' from the BFS pages.
-
LC 130 asks you to capture every region NOT touching the border. Why not test each region separately?
Boundary inversion is the trick behind LC 130, 1020 and 417. Deciding 'does this region touch the border' region-by-region is both more code and easy to get wrong; starting from the border makes the answer structural.
pch.quizShowAnswer
B — Because it inverts the work: flood inward from the border cells to mark the surviving regions, then flip everything unmarked — one pass instead of a border test per region — Boundary inversion is the trick behind LC 130, 1020 and 417. Deciding 'does this region touch the border' region-by-region is both more code and easy to get wrong; starting from the border makes the answer structural.
Recall card
Section titled “Recall card”- Cue — a 2-D grid where adjacent same-valued cells form regions: count them, measure them, or recolour one.
- Idea — the grid is the graph; the four offsets
(1,0) (-1,0) (0,1) (0,-1)are the adjacency function. Nothing gets built. - Template — base case checks bounds and validity; mark before recursing; the outer double loop counts sink starts, which is the component count.
- Visited — sink the grid (mutates the caller’s input) or a
visitedset ( space). Say which and why. - Cost — time; the recursion is deep worst case, so use an explicit stack or BFS on large grids. ” space” is a wrong answer.
- Boundary inversion — for “regions not touching the edge” (LC 130, 1020, 417), flood inward from the border and invert.
- Read the cell type —
"1"in LC 200,1in LC 695; diagonals are not adjacent under 4-directional rules.
- A grid is an implicit graph: the four neighbour offsets are the adjacency function, so no graph needs building.
- The base case does bounds plus validity in one place; the outer double loop counts components.
- Sink the grid for space, or keep a
visitedset to avoid mutating the input — and say which you chose. - DFS and BFS are interchangeable for counting; only BFS gives shortest paths.
- Mark before recursing, or cells get counted twice and recursion can loop.
- Boundary inversion — flood inward from the border and invert — is the key to 130, 1020 and 417. Do not test regions one at a time.
- Watch Python’s recursion limit on large solid grids; an explicit stack is the safe default.
Next: Multi-source BFS — what to do when the search starts from many cells at once, and why that is still a single pass.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading