Number of Closed Islands: DFS Boundary Elimination
You are given a 2D grid of 0s and 1s, where 0 represents land and 1 represents water. An island is a group of 0s connected 4-directionally. A closed island is an island that is completely surrounded by water, meaning none of its cells touch the boundary of the grid. Return the number of closed islands.
This is LeetCode 1254: Number of Closed Islands, and it is one of the cleanest problems for learning the boundary elimination pattern. The trick it teaches applies to any grid problem where touching the edge disqualifies a region.
The two-pass approach
The core insight is that any island connected to the grid boundary cannot be "closed." So instead of checking every island to see whether it touches an edge, you flip the problem around: first, eliminate all boundary-connected land. Then, count whatever land remains. Every remaining island is guaranteed to be closed.
This gives you a clean two-pass algorithm:
- Pass 1 (eliminate): Walk along all four edges of the grid. Whenever you find a land cell (
0) on the boundary, run DFS from it and convert every connected land cell to water (1). This "sinks" all boundary-connected islands. - Pass 2 (count): Scan the entire grid. Whenever you find a land cell (
0), increment your count and run DFS to sink that entire island so you do not count it again.
After both passes, your count holds the number of closed islands.
The solution
def closed_island(grid: list[list[int]]) -> int:
rows, cols = len(grid), len(grid[0])
def dfs(r: int, c: int) -> None:
if r < 0 or r >= rows or c < 0 or c >= cols:
return
if grid[r][c] != 0:
return
grid[r][c] = 1
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
for r in range(rows):
for c in range(cols):
if (r == 0 or r == rows - 1 or c == 0 or c == cols - 1) and grid[r][c] == 0:
dfs(r, c)
count = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 0:
count += 1
dfs(r, c)
return count
Let's walk through what each piece does.
The dfs function is a standard flood fill. It takes a cell, checks bounds and whether the cell is land, then marks it as water and recurses in all four directions. This is the same DFS you use in Number of Islands.
The first double loop handles boundary elimination. It iterates over every cell, but the if condition restricts it to boundary cells only (first row, last row, first column, last column). When it finds land on the boundary, it calls dfs to sink the entire connected component. After this loop, no land cell in the grid touches any edge.
The second double loop counts the closed islands. Every land cell it encounters is the start of a new island that is fully surrounded by water. It increments the counter and calls dfs to sink that island, preventing double-counting.
This "eliminate then count" pattern is reusable. Whenever a problem asks you to ignore regions that touch the boundary, your first instinct should be to walk the edges, DFS-sink everything connected to them, and then solve the simpler remaining problem.
Visual walkthrough
Let's trace through a 5x5 grid step by step. Watch how boundary elimination simplifies the counting pass.
Step 1: Identify the grid. Land cells (0) form two groups.
Green = land (0). Blue = water (1). The bottom row has land touching the boundary at (3,0).
Step 2: Mark boundary-connected land. DFS from (3,0) finds all connected land on the edge.
Red = boundary-connected land. DFS from the border cell at row 3 marks (3,0), (3,1), (3,2), and (3,3).
Step 3: Sink the boundary land. Convert marked cells to water so they are no longer counted.
Boundary land is now treated as water (dark cells). Only interior land remains.
Step 4: Count remaining islands. DFS from (1,1) finds one closed island. Final answer: 1.
Yellow = closed island. This group of land is fully surrounded by water, so it counts. Total closed islands: 1.
After sinking the boundary-connected land in steps 2 and 3, the only land left forms a single closed island. The counting pass in step 4 picks it up immediately.
Complexity analysis
| Approach | Time | Space |
|---|---|---|
| DFS boundary elimination | O(m * n) | O(m * n) |
Time is O(m * n) where m is the number of rows and n is the number of columns. The boundary elimination pass visits each cell at most once (DFS marks cells as water, so they are never revisited). The counting pass also visits each cell at most once. Two passes over the grid is still O(m * n).
Space is O(m * n) in the worst case due to the recursion stack. If the entire grid is land, the DFS call stack can grow to m * n frames deep. In practice, the depth is bounded by the size of the largest connected component. You can convert this to iterative DFS with an explicit stack if stack overflow is a concern.
The building blocks
1. Boundary-connected DFS elimination
The pattern of walking the grid edges and sinking everything reachable from boundary land:
for r in range(rows):
for c in range(cols):
if (r == 0 or r == rows - 1 or c == 0 or c == cols - 1) and grid[r][c] == 0:
dfs(r, c)
This is the same approach used in Surrounded Regions (LeetCode 130), where you mark boundary-connected O cells as safe before flipping the rest. Any time a problem says "ignore regions that touch the edge," this building block is your tool.
2. DFS flood fill for island counting
The pattern of scanning the grid, finding unvisited land, and sinking the entire island:
count = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 0:
count += 1
dfs(r, c)
This is identical to the counting logic in Number of Islands (LeetCode 200). You find a land cell, increment your count, and flood-fill to mark the whole island as visited. The combination of boundary elimination followed by this counting pass is what makes the closed islands solution work.
Edge cases
Before submitting, think through these scenarios:
- All water: every cell is
1. No land exists, so return0. - All land: every cell is
0. The boundary elimination sinks everything, so return0since no island can be closed. - Single row or single column: every land cell touches the boundary. Return
0. - 1x1 grid:
[[0]]has land on the boundary, so it is not closed. Return0.[[1]]is all water. Return0. - Land only in the interior: no boundary land to eliminate. The counting pass finds all islands, and each one is closed.
- Multiple separate closed islands: the counting pass finds each one independently. Each DFS call sinks one island and increments the count.
From understanding to recall
You have seen how boundary elimination plus flood fill counting solves closed islands. The logic has two clean phases, and the code reuses the same DFS helper for both. But reproducing it under pressure requires more than understanding.
The details that trip people up are small. Do you check the boundary condition with or or and? Do you convert land to water with grid[r][c] = 1 or something else? Do you run the elimination pass before or after counting? These are not conceptual gaps. They are recall gaps, and they show up when the clock is ticking.
Spaced repetition closes them. You practice the boundary walk, the DFS flood fill, and the two-pass structure from memory at increasing intervals. After a few rounds, the pattern is automatic. You see "closed island" or "surrounded region" in a problem description, and the boundary elimination template flows out without hesitation.
Related posts
- Number of Islands - The foundational grid DFS problem
- Surrounded Regions - Similar boundary-connected elimination pattern
- Flood Fill - Basic DFS flood fill on a grid
CodeBricks breaks Number of Closed Islands into its boundary elimination and flood fill counting building blocks, then drills them independently with spaced repetition. You type each piece from scratch until the pattern is automatic. When a grid problem shows up in your interview, you do not think about it. You just write it.