题目描述
思路解析动画文字版
看一眼这个网格:紫色是陆地(0),蓝色是海水(1)。先肉眼找找陆地块:左上角 (0,0) 一格、中间偏左 (1,1)(1,2)(2,1) 一块、右边 (2,4)(3,4) 一块。一共三块陆地。
认陆地 · 第一块在角上:先单看左上角这块(红色):它就在网格的最外圈拐角,明摆着漏到了外面。这种贴边的陆地,待会儿一定不算封闭岛。其余两块先压暗。
认陆地 · 第二块在内部:再看中间这块 L 形(紫色):(1,1)(1,2)(2,1) 三格,四周一圈全是海水,一格都不挨边。它很可能就是个封闭岛。
认陆地 · 第三块也在内部:右边这块(紫色):(2,4)(3,4) 两格,同样被海水围在网格内部,不挨边。所以肉眼判断:封闭岛应该是中间和右边这两块。下面用算法严格走一遍。
先想最直白的判断:最直白的想法:先把每一块连成片的陆地圈出来,再逐块判断「这块里有没有哪一格贴着网格边缘」。碰到边的就淘汰,没碰到边的才计数。思路没错,但分两步走有点啰嗦。
关键反转:换个角度:凡是能连到网格边缘的陆地,都注定不是封闭岛。那我们干脆先把它们全淹掉(改成海水)。淹完之后,网格里剩下的每一块陆地,必然四面被水围死——直接数就行。
为什么这样是对的:淹掉所有「挨边」的陆地后,再没有任何一块陆地能漏到外面。这时网格里还剩几块陆地,封闭岛就有几个。两个动作:先淹边、再数岛,都用同一个工具——DFS(深度优先搜索,从一格出发把整片连通陆地走遍)。
第一步 · 扫描最外圈:先沿网格最外一圈找陆地。最外圈里只有 (0,0) 是陆地(标红=要被淹)。其它边缘格都是海水,跳过。中间两块陆地暂时还是紫色。
淹边界 · 从 (0,0) 出发 DFS:在 (0,0) 这个边缘陆地上启动 DFS,把它压进栈。接下来要把所有和它连成一片的陆地都淹掉。
淹边界 · 淹掉 (0,0):把 (0,0) 改成海水(它从紫变蓝)。再看它上下左右四个邻居——全是海水,没有别的陆地能扩,这块「挨边陆地」就一格,淹完出栈。边缘那块没了。
继续扫外圈 · 没有别的边缘陆地:把最外圈剩下的格子扫完,全是海水了,没有别的「挨边陆地」要淹。第一步完成:所有能漏到外面的陆地都已清掉,网格里只剩两块紫色陆地。
第二步开始:现在剩下的每块陆地都是封闭岛。从左上到右下逐格扫,第一次碰到某块陆地就把答案 +1,然后用 DFS 把这一整片淹掉(防止同一块被重复数)。继续扫,再碰到新陆地再 +1。
数岛 · 碰到第 1 块封闭岛:扫到 (1,1),发现一格还没淹的陆地!这一定是个封闭岛(挨边的早被淹了)。封闭岛计数 = 1,标红,并从它启动 DFS 把这一整块走遍。
数岛 · DFS 淹这块 · 走到 (1,2):把 (1,1) 淹成海水(变蓝),DFS 顺着它的邻居走到右边的 (1,2),也是陆地,压栈,继续。这一整块连通陆地我们要一格不漏地踩遍。
数岛 · DFS 淹这块 · 走到 (2,1):把 (1,2) 也淹掉。DFS 回头探到 (1,1) 下方的 (2,1),又是陆地,继续淹。这块岛是 L 形的三格:(1,1)(1,2)(2,1)。
数岛 · 淹掉 (2,1) · 探四邻:把 (2,1) 也淹成海水(变蓝),再看它上下左右四个邻居——全是海水,没有新陆地可扩。这块 L 形岛就到此为止,准备回退出栈。
数岛 · 第 1 块淹完:(2,1) 的四周再没有陆地了,这块岛 (1,1)(1,2)(2,1) 全淹完、栈空。第 1 个封闭岛确认。网格里只剩右边一块紫色陆地。
数岛 · 碰到第 2 块封闭岛:继续往后扫,在 (2,4) 又碰到一格没淹的陆地。封闭岛计数 = 2,标红,启动 DFS 淹它。
数岛 · DFS 淹这块 · 走到 (3,4):淹掉 (2,4),DFS 往下走到 (3,4),也是陆地,压栈继续。这块岛是竖着的两格:(2,4)(3,4)。
数岛 · 第 2 块淹完 · 全部结束:(3,4) 四周无陆地,这块淹完、栈空。继续扫到底,整张网格再没有一格陆地。扫描结束,封闭岛数目 = 2。
回看整套流程:整道题就两步:先淹边,把所有挨着网格边缘、能漏到外面的陆地清空;再数岛,从头扫一遍,剩下的每块独立陆地就是一个封闭岛。淹掉是为了不重复数同一块。
回看 · 被淘汰的挨边块:回到原始网格对照一下。红色的 (0,0) 因为贴着边缘、能漏到外面,第①遍就被淹掉、不计入。下面看看真正被算进去的两块。
回看 · 第 1 个封闭岛:第 1 个封闭岛(绿色):(1,1)(1,2)(2,1) 这块 L 形陆地,上下左右一圈全是海水,被彻底围死,算 1 个。
回看 · 第 2 个封闭岛:第 2 个封闭岛(绿色):(2,4)(3,4) 竖着两格,同样被海水四面围住。两块封闭岛,答案 = 2,和算法跑出来的一致。
本质金句:凡是「不与边界相连 / 被完全包围」这类网格题,套路都是:先从四条边出发,把能连到边的目标全部清除,剩下的天然满足「被围死」,直接数或标记即可。
易错实演 · 忘了先淹边:如果跳过「先淹边」,直接数所有陆地块,会把左上角挨边的 (0,0)(红色)也当成封闭岛,得出错误的 3。它明明漏到了网格外面,根本不封闭——这就是必须先淹边的原因。
边界三连:全海水、陆地全挨边这两种都返回 0;只要内部有哪怕一块被水围死的陆地,就至少数到 1。先淹边的写法把这些极端情况都自然兜住了。
面试追问:面试常拿它和 LC200 对比考「先淹边」这步,以及追问 BFS / 并查集 / 防爆栈的替代写法,提前想清楚答得更稳。
参考代码
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 count复杂度
- 时间:O(m·n),每个格子最多被 DFS 访问一次,两遍扫描合起来仍是格子总数级
- 空间:O(m·n),递归栈最坏深度为格子总数(整张图连成一片陆地时)
易错点
面试追问把动画讲成自己的话
追问这题和 LC200 岛屿数量有什么区别?
追问能用 BFS 或并查集做吗?
追问如果网格特别大,递归 DFS 会爆栈怎么办?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
连通网络的操作次数
LeetCode 1319 · 中等 · 沿着 图论套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题