LeetCode 1254中等网格 DFS
统计封闭岛屿的数目 图解题解
这道题到底在问什么
给定一个二维网格 grid,0 表示陆地,1 表示海水。一个「封闭岛屿」是指:完全由海水包围、且不与网格边缘相连的那一整片陆地。请返回封闭岛屿的数目。
- 输入
- grid = 见下方网格(0=陆地,1=海水)
- 输出
- 2
- 解释
- 挨着边的那块陆地不算;网格内部被水围死的两块才算封闭岛。
最优解:一步一步想明白
- 30=陆地(紫) · 1=海水(蓝)紫色是陆地(0),蓝色是海水(1)。先肉眼找找陆地块:左上角 (0,0) 一格、中间偏左 (1,1)(1,2)(2,1) 一块、右边 (2,4)(3,4) 一块。一共三块陆地。
- 4(0,0) 贴着网格边缘先单看左上角这块(红色):它就在网格的最外圈拐角,明摆着漏到了外面。这种贴边的陆地,待会儿一定不算封闭岛。其余两块先压暗。
- 5(1,1)(1,2)(2,1) 不挨边再看中间这块 L 形(紫色):(1,1)(1,2)(2,1) 三格,四周一圈全是海水,一格都不挨边。它很可能就是个封闭岛。
- 6(2,4)(3,4) 也不挨边右边这块(紫色):(2,4)(3,4) 两格,同样被海水围在网格内部,不挨边。所以肉眼判断:封闭岛应该是中间和右边这两块。下面用算法严格走一遍。
- 7对每块陆地:会不会碰到边?最直白的想法:先把每一块连成片的陆地圈出来,再逐块判断「这块里有没有哪一格贴着网格边缘」。碰到边的就淘汰,没碰到边的才计数。思路没错,但分两步走有点啰嗦。
- 8先把「会漏的」全淹掉换个角度:凡是能连到网格边缘的陆地,都注定不是封闭岛。那我们干脆先把它们全淹掉(改成海水)。淹完之后,网格里剩下的每一块陆地,必然四面被水围死——直接数就行。
- 9淹完 = 只剩封闭岛淹掉所有「挨边」的陆地后,再没有任何一块陆地能漏到外面。这时网格里还剩几块陆地,封闭岛就有几个。两个动作:先淹边、再数岛,都用同一个工具——DFS(深度优先搜索,从一格出发把整片连通陆地走遍)。
- 10找边缘上的陆地先沿网格最外一圈找陆地。最外圈里只有 (0,0) 是陆地(标红=要被淹)。其它边缘格都是海水,跳过。中间两块陆地暂时还是紫色。
- 11踩到边缘陆地,开淹在 (0,0) 这个边缘陆地上启动 DFS,把它压进栈。接下来要把所有和它连成一片的陆地都淹掉。
- 12陆地 0 → 海水 1把 (0,0) 改成海水(它从紫变蓝)。再看它上下左右四个邻居——全是海水,没有别的陆地能扩,这块「挨边陆地」就一格,淹完出栈。边缘那块没了。
- 13外圈剩下全是海水把最外圈剩下的格子扫完,全是海水了,没有别的「挨边陆地」要淹。第一步完成:所有能漏到外面的陆地都已清掉,网格里只剩两块紫色陆地。
- 14数剩下的每一块陆地现在剩下的每块陆地都是封闭岛。从左上到右下逐格扫,第一次碰到某块陆地就把答案 +1,然后用 DFS 把这一整片淹掉(防止同一块被重复数)。继续扫,再碰到新陆地再 +1。
- 15在 (1,1) 发现陆地 → 计数+1扫到 (1,1),发现一格还没淹的陆地!这一定是个封闭岛(挨边的早被淹了)。封闭岛计数 = 1,标红,并从它启动 DFS 把这一整块走遍。
- 16右邻也是陆地,继续走把 (1,1) 淹成海水(变蓝),DFS 顺着它的邻居走到右边的 (1,2),也是陆地,压栈,继续。这一整块连通陆地我们要一格不漏地踩遍。
- 17回头走 (1,1) 下方的 (2,1)把 (1,2) 也淹掉。DFS 回头探到 (1,1) 下方的 (2,1),又是陆地,继续淹。这块岛是 L 形的三格:(1,1)(1,2)(2,1)。
- 18(2,1) 变海水,四周已无陆地把 (2,1) 也淹成海水(变蓝),再看它上下左右四个邻居——全是海水,没有新陆地可扩。这块 L 形岛就到此为止,准备回退出栈。
- 19这块岛三格全变海水(2,1) 的四周再没有陆地了,这块岛 (1,1)(1,2)(2,1) 全淹完、栈空。第 1 个封闭岛确认。网格里只剩右边一块紫色陆地。
- 20扫到 (2,4) 陆地 → 计数+1继续往后扫,在 (2,4) 又碰到一格没淹的陆地。封闭岛计数 = 2,标红,启动 DFS 淹它。
- 21下邻也是陆地,继续淹淹掉 (2,4),DFS 往下走到 (3,4),也是陆地,压栈继续。这块岛是竖着的两格:(2,4)(3,4)。
- 22网格再无陆地 → 扫描结束(3,4) 四周无陆地,这块淹完、栈空。继续扫到底,整张网格再没有一格陆地。扫描结束,封闭岛数目 = 2。
- 23两遍 DFS 解决整道题就两步:先淹边,把所有挨着网格边缘、能漏到外面的陆地清空;再数岛,从头扫一遍,剩下的每块独立陆地就是一个封闭岛。淹掉是为了不重复数同一块。
- 24(0,0) 漏到外面,不计回到原始网格对照一下。红色的 (0,0) 因为贴着边缘、能漏到外面,第①遍就被淹掉、不计入。下面看看真正被算进去的两块。
- 25(1,1)(1,2)(2,1) 四面被围第 1 个封闭岛(绿色):(1,1)(1,2)(2,1) 这块 L 形陆地,上下左右一圈全是海水,被彻底围死,算 1 个。
- 26(2,4)(3,4) 同样被围第 2 个封闭岛(绿色):(2,4)(3,4) 竖着两格,同样被海水四面围住。两块封闭岛,答案 = 2,和算法跑出来的一致。
- 29记住这一句凡是「不与边界相连 / 被完全包围」这类网格题,套路都是:先从四条边出发,把能连到边的目标全部清除,剩下的天然满足「被围死」,直接数或标记即可。
- 31不淹边就直接数 → 多数 1如果跳过「先淹边」,直接数所有陆地块,会把左上角挨边的 (0,0)(红色)也当成封闭岛,得出错误的 3。它明明漏到了网格外面,根本不封闭——这就是必须先淹边的原因。
⚠️ 容易写错的地方
✗ 错:把 0 当海水、1 当陆地
✓ 对:本题 0=陆地、1=海水,和 LC200 反过来
数反了陆海,整道题逻辑全错,务必先看清题目定义
✗ 错:直接数所有陆地块当封闭岛
✓ 对:必须先淹掉挨边的陆地再数
挨边的陆地能漏到外面,不是封闭岛,不淹掉会多数
✗ 错:数完一块陆地忘了把它淹掉
✓ 对:数到 +1 后立刻 DFS 把整块淹成海水
不淹掉,同一块陆地的每一格都会各被数一次,结果暴涨
完整代码(Python / C++ / Java)
Python
class Solution:
def closedIsland(self, grid):
m, n = len(grid), len(grid[0])
def dfs(i, j):
if i < 0 or i >= m or j < 0 or j >= n:
return
if grid[i][j] != 0:
return
grid[i][j] = 1 # 淹成海水
dfs(i - 1, j); dfs(i + 1, j)
dfs(i, j - 1); dfs(i, j + 1)
for i in range(m): # ① 淹掉边界相连的陆地
for j in range(n):
if i == 0 or i == m - 1 or j == 0 or j == n - 1:
if grid[i][j] == 0:
dfs(i, j)
count = 0
for i in range(m): # ② 数剩下的封闭岛
for j in range(n):
if grid[i][j] == 0:
dfs(i, j)
count += 1
return countC++
class Solution {
public:
int m, n;
void dfs(vector<vector<int>>& g, int i, int j) {
if (i < 0 || i >= m || j < 0 || j >= n) return;
if (g[i][j] != 0) return;
g[i][j] = 1; // 淹成海水
dfs(g, i - 1, j); dfs(g, i + 1, j);
dfs(g, i, j - 1); dfs(g, i, j + 1);
}
int closedIsland(vector<vector<int>>& grid) {
m = grid.size(); n = grid[0].size();
for (int i = 0; i < m; i++) // ① 淹边界
for (int j = 0; j < n; j++)
if (i == 0 || i == m - 1 || j == 0 || j == n - 1)
if (grid[i][j] == 0) dfs(grid, i, j);
int count = 0;
for (int i = 0; i < m; i++) // ② 数封闭岛
for (int j = 0; j < n; j++)
if (grid[i][j] == 0) {
dfs(grid, i, j);
count++;
}
return count;
}
};Java
class Solution {
int m, n;
void dfs(int[][] g, int i, int j) {
if (i < 0 || i >= m || j < 0 || j >= n) return;
if (g[i][j] != 0) return;
g[i][j] = 1; // 淹成海水
dfs(g, i - 1, j); dfs(g, i + 1, j);
dfs(g, i, j - 1); dfs(g, i, j + 1);
}
public int closedIsland(int[][] grid) {
m = grid.length; n = grid[0].length;
for (int i = 0; i < m; i++) { // ① 淹边界
for (int j = 0; j < n; j++) {
if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
if (grid[i][j] == 0) dfs(grid, i, j);
}
}
}
int count = 0;
for (int i = 0; i < m; i++) { // ② 数封闭岛
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) {
dfs(grid, i, j);
count++;
}
}
}
return count;
}
}复杂度
时间
O(m·n)
每个格子最多被 DFS 访问一次,两遍扫描合起来仍是格子总数级
空间
O(m·n)
递归栈最坏深度为格子总数(整张图连成一片陆地时)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 统计封闭岛屿的数目 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和 LC200 岛屿数量有什么区别?+
LC200 数所有岛屿,1 是陆地;本题 0 是陆地,且只数「不挨边」的封闭岛。核心多了一步「先沿边界把挨边的陆地淹掉」,再数剩下的,框架都是网格 DFS。
能用 BFS 或并查集做吗?+
可以。BFS 把递归换成队列、淹的逻辑一样;并查集则把陆地格两两合并,再判断每个连通块里有没有边界格。三种都行,DFS 代码最短最直观。
如果网格特别大,递归 DFS 会爆栈怎么办?+
改成显式栈的迭代 DFS,或用 BFS 队列,把递归深度从调用栈挪到堆上,避免栈溢出;逻辑完全等价。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 统计封闭岛屿的数目 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。