LeetCode 1254 - Number of Closed Islands
This problem provides a 2D grid representing a map of land and water. Cells with 0 represent land, and cells with 1 repr
Difficulty: 🟡 Medium
Topics: Array, Depth-First Search, Breadth-First Search, Union-Find, Matrix
Solution
Problem Understanding
This problem provides a 2D grid representing a map of land and water. Cells with 0 represent land, and cells with 1 represent water. An island is any contiguous region of land connected horizontally or vertically (not diagonally). A closed island is an island that is entirely surrounded by water on all four sides, including edges of the grid.
The task is to count how many closed islands exist in the given grid. The input grid can be as large as 100 by 100, meaning a naive algorithm must be efficient enough to handle up to 10,000 cells. The problem guarantees that each cell is either 0 or 1, so we do not need to handle invalid cell values.
Important edge cases include grids where islands touch the grid edges (they cannot be closed), grids entirely filled with water (no islands), and single-cell grids that may or may not form a closed island.
Approaches
The brute-force approach is to iterate over every land cell (0) and perform a depth-first search (DFS) or breadth-first search (BFS) to explore the entire connected island. While exploring, we track whether any part of the island touches the border of the grid. If it does, we mark it as not closed; otherwise, we count it. This approach works but may repeatedly traverse the same cells unnecessarily, and if not carefully implemented, it could use extra space for visited cells.
The optimal approach leverages the observation that any island touching the border cannot be closed. We first flood-fill all land cells connected to the border, marking them as visited or water to ignore them. After this pre-processing step, we iterate over the remaining land cells. Each DFS/BFS from these cells is guaranteed to be a closed island. This reduces unnecessary checks and simplifies logic.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(m*n) | O(m*n) | Explore every land cell; track visited cells; check borders during DFS/BFS |
| Optimal | O(m*n) | O(m*n) | Pre-flood-fill border-connected land; remaining islands are all closed |
Algorithm Walkthrough
- Iterate through all border cells of the grid (top row, bottom row, leftmost column, rightmost column). For each land cell (
0), perform DFS to mark all connected land as visited or converted to water (1). This ensures that no border-connected island will be counted later. - Initialize a counter
closed_islands = 0. - Iterate over all cells of the grid. For each unvisited land cell (
0), perform DFS to mark all connected land as visited and increment theclosed_islandscounter by one. - Return
closed_islandsas the total number of closed islands.
Why it works: The invariant maintained is that all islands touching the grid edges are removed before counting. Therefore, any remaining land island is fully enclosed by water and meets the closed island definition. DFS ensures all connected land cells are visited in each step, guaranteeing that no island is counted multiple times.
Python Solution
from typing import List
class Solution:
def closedIsland(self, grid: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])
def dfs(r: int, c: int):
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == 1:
return
grid[r][c] = 1
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
# Flood-fill border-connected lands
for r in range(rows):
dfs(r, 0)
dfs(r, cols - 1)
for c in range(cols):
dfs(0, c)
dfs(rows - 1, c)
closed_islands = 0
for r in range(1, rows - 1):
for c in range(1, cols - 1):
if grid[r][c] == 0:
dfs(r, c)
closed_islands += 1
return closed_islands
Explanation: The dfs function marks all connected land cells as water to avoid recounting. First, all border-connected land cells are removed, ensuring only potential closed islands remain. The nested loops then search for remaining land cells and count each isolated DFS as a closed island.
Go Solution
func closedIsland(grid [][]int) int {
rows, cols := len(grid), len(grid[0])
var dfs func(r, c int)
dfs = func(r, c int) {
if r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] == 1 {
return
}
grid[r][c] = 1
dfs(r+1, c)
dfs(r-1, c)
dfs(r, c+1)
dfs(r, c-1)
}
// Flood-fill border-connected lands
for r := 0; r < rows; r++ {
dfs(r, 0)
dfs(r, cols-1)
}
for c := 0; c < cols; c++ {
dfs(0, c)
dfs(rows-1, c)
}
closedIslands := 0
for r := 1; r < rows-1; r++ {
for c := 1; c < cols-1; c++ {
if grid[r][c] == 0 {
dfs(r, c)
closedIslands++
}
}
}
return closedIslands
}
Explanation: Go handles slices and DFS similarly to Python. The function uses recursion to mark visited land cells. Go does not require type hints for inner functions, and the indexing and loop logic remain consistent with the Python approach.
Worked Examples
Example 1:
Grid:
1 1 1 1 1 1 1 0
1 0 0 0 0 1 1 0
1 0 1 0 1 1 1 0
1 0 0 0 0 1 0 1
1 1 1 1 1 1 1 0
Step 1: Flood-fill border lands converts the rightmost column of 0s to 1.
Step 2: DFS iterates from internal cells. The first internal island (top-left) is counted as closed. The second internal island (bottom-left) is counted as closed.
Output: 2
Example 2:
Grid:
0 0 1 0 0
0 1 0 1 0
0 1 1 1 0
Border DFS removes top-left and top-right land touching the borders. One central island remains.
Output: 1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m*n) | Each cell is visited at most once during DFS flood-fill and counting. |
| Space | O(m*n) | Recursive DFS stack may grow up to the size of the largest island in the worst case. |
The algorithm is efficient for the given constraints since it processes each cell a constant number of times. Recursive stack depth is limited by the island size.
Test Cases
# Provided examples
assert Solution().closedIsland([[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]]) == 2
assert Solution().closedIsland([[0,0,1,0,0],[0,1,0,1,0],[0,1,1,1,0]]) == 1
assert Solution().closedIsland([[1,1,1,1,1,1,1],[1,0,0,0,0,0,1],[1,0,1,1,1,0,1],[1,0,1,0,1,0,1],[1,0,1,1,1,0,1],[1,0,0,0,0,0,1],[1,1,1,1,1,1,1]]) == 2
# Edge cases
assert Solution().closedIsland([[1]]) == 0 # single water
assert Solution().closedIsland([[0]]) == 0 # single land on border, not closed
assert Solution().closedIsland([[1,1,1],[1,0,1],[1,1,1]]) == 1 # 1 closed island inside border
assert Solution().closedIsland([[0,0,0],[0,0,0],[0,0,0]]) == 0 # all land touches borders
assert Solution().closedIsland([[1,1,1],[1,1,1],[1,1,1]]) == 0 # all water
| Test | Why |
|---|---|
| Example 1 | Multiple closed islands with some islands touching borders |
| Example 2 | Single central closed island |
| Example 3 | Multiple islands fully enclosed |
| Single water cell | Minimal grid with no |