Closed Islands
Grid flood fill with one twist: an island only counts if it never touches the edge. Sink everything connected to the border first, then count what is left.
- Pattern
- Border-seeded flood fill — sink every island touching the edge, then count what remains
- Difficulty
- Medium
- Time
- O(m·n)
- Space
- O(m·n)
Given a grid of 0s (land) and 1s (water), count the closed islands: groups of land connected up/down/left/right that are completely surrounded by water. An island that touches the edge of the grid is not closed. (LC 1254)
This is the question that gets dressed up as manufacturing. A CNC machine drills into a sheet of metal, and you need to tell a hole from a cut-out on the edge. Read 1 as metal and 0 as void. A void that never reaches the edge is a hole. A void that touches the edge is just the sheet's outline, so it does not count.
It is Number of Islands with one extra rule, and the rule changes where you start.
The recipe
Say before you type: "An island touching the edge can't be closed. So before I count anything, I flood every island connected to the border into water. Whatever land is left can't reach the edge, so each island I find after that is closed."
- Flood-fill from every border cell that is land, turning it into water. That erases every island that touches the edge.
- Scan the interior. Each remaining
0is a new closed island: count it, then flood it so you never count it twice. - The flood's base case does all the guarding: out of bounds, or not land → return.
- Mark the cell before recursing, same as Number of Islands. The mark is the visited set.
The code
function closedIsland(grid: number[][]): number {
const rows = grid.length, cols = grid[0].length;
const flood = (r: number, c: number): void => {
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] !== 0) return;
grid[r][c] = 1; // mark visited IMMEDIATELY
flood(r + 1, c); flood(r - 1, c); flood(r, c + 1); flood(r, c - 1);
};
// 1. Erase every island that touches the edge.
for (let r = 0; r < rows; r++) { flood(r, 0); flood(r, cols - 1); }
for (let c = 0; c < cols; c++) { flood(0, c); flood(rows - 1, c); }
// 2. What is left cannot reach the edge. Count it.
let count = 0;
for (let r = 1; r < rows - 1; r++)
for (let c = 1; c < cols - 1; c++)
if (grid[r][c] === 0) { count++; flood(r, c); }
return count;
}
Why this shape
"Is this island closed?" is a question about the whole island: one cell touching the edge disqualifies all of it. Asking it island by island makes you carry that fact through the search. Flipping it around is simpler. Everything reachable from the border is disqualified, so remove it first, and the rest is plain Number of Islands.
This is the border-seeded flood fill, and it is worth knowing by name. It shows up whenever the answer depends on reaching the boundary: Surrounded Regions (130), Pacific Atlantic Water Flow (417), Number of Enclaves (1020). All three start the search from the edge instead of from the thing you are asked about.
Complexity
| Cost | Because | |
|---|---|---|
| Time | O(m·n) | Each cell is flooded at most once, and the border scan and interior scan each touch a cell a constant number of times |
| Space | O(m·n) | Worst-case recursion depth: one snake-shaped island |
Traps
- The encoding is flipped. In Number of Islands,
'1'is land and the grid holds strings. Here0is land,1is water, and the grid holds numbers. Writing=== 1from muscle memory counts the water. - Counting before you clear the border. If you count in the first pass, every edge island is counted too. The order is the whole algorithm: clear first, count second.
- Skipping the corners and the other edge. Flood from all four sides: the first and last column of every row, and the first and last row of every column.
- Test a non-square grid. Mixing up
rowsandcolsin the bounds check passes every square test and only breaks on a wide or tall one. - Diagonals are not connected. Two
0s touching at a corner are two separate holes.
The other way: one pass that reports back
You can also decide closed-ness during the search. Each flood returns whether its island stayed enclosed, and walking off the grid means it escaped:
function closedIsland(grid: number[][]): number {
const rows = grid.length, cols = grid[0].length;
// True if this island is closed. Always explores the WHOLE island.
const explore = (r: number, c: number): boolean => {
if (r < 0 || r >= rows || c < 0 || c >= cols) return false; // walked off the sheet
if (grid[r][c] !== 0) return true; // metal, or already visited, is a wall
grid[r][c] = 1;
const down = explore(r + 1, c);
const up = explore(r - 1, c);
const right = explore(r, c + 1);
const left = explore(r, c - 1);
return down && up && right && left; // combine AFTER all four have run
};
let count = 0;
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++)
if (grid[r][c] === 0 && explore(r, c)) count++;
return count;
}
The trap is the last line. Writing return explore(r+1,c) && explore(r-1,c) && ... short-circuits: the first false stops the rest of the island from being explored. The unvisited cells stay 0, the outer scan finds them later, and a single island is counted again or wrongly accepted. Run all four calls first, then combine.
It is fine to offer either version. The border-first one is easier to get right under pressure, and saying why is a good signal.
The pattern this trains
Grid flood fill is a family. Number of Islands (200) counts components. Max Area of Island (695) measures them. Closed Islands (1254) adds a boundary condition, and the border-seeded flood fill answers it. Rotting Oranges (994) swaps the stack for a queue to measure time. The question to ask is what is the search seeded from: every cell (count things), the edge (reach the boundary), or a set of sources at once (spread over time).