岛屿数量:见陆就淹
数的不是陆地格子,是四连通的陆地块。每发现一块没见过的陆地,就 DFS 把整座岛淹掉。
网格是隐式图:每个 '1' 是顶点,上下左右的 '1' 是边。遍历每个格子,遇到还露着的陆地就岛屿数 +1,再 DFS 把四连通的 '1' 全部原地改成 '0'。每座岛恰好被发现一次。时间 O(m·n),空间 O(m·n)。
这是 LeetCode 200. Number of Islands。大白话:给你一张由 '1'(陆地)和 '0'(水)组成的二维网格,上下左右相连的陆地算一座岛,请数有几座。对角线不相连。
主例是这张 3×3 的图:第一行 1 1 0,第二行 1 0 0,第三行 0 0 1。左上 (0,0)、(0,1)、(1,0) 三块陆地连成一座岛;右下 (2,2) 独自一块。中间隔着水,所以答案是 2,不是 4。
第一直觉是看见一个 '1' 就计数。那样你会把左上三格数成 3,再加上右下,得到 4——你数的是陆地格子,不是连通块。缺的是一次出发把整座岛标记完的办法。下面从这块缺口推出「发现即淹没」。
DFS 淹没整座岛
输入给的是二维字符数组,不是画好的点和边。但每个陆地格子的上下左右若也是陆地,那就是一条边。顶点是格子,邻接关系写在坐标加减里,这种图叫隐式图。水不是顶点:你不会从陆地上岸走到水里再上岸。
缺的信息是「这座岛我数过没有」。没有标记,扫到 (0,0) 加 1,扫到 (0,1) 再加 1,同一座岛被加爆。反过来,只从 (0,0) 出发却不记访问,DFS 会在 (0,0) 和 (0,1) 之间来回——网格四连通是无向的,回头边天然存在,树遍历那套「不必 visited」在这里会炸栈。
从缺口反推两层动作。外层从左上扫到右下:遇到还是 '1' 的格子,说明发现了一座新岛,count +1,然后把淹没的任务交给 DFS。内层 DFS 的契约是:从当前格出发,把四连通的陆地全部改成 '0'。越界、是水、或已经淹过,都当作没有这条边,直接返回。走进一格立刻改成 '0',防止环把你转回来。
主例从 (0,0) 开始。它是 '1',count 变成 1,进入淹没。把 (0,0) 改成水,下是 (1,0)、右是 (0,1),两块都是陆地,依次钻进去改成水。三块陆地淹完,网格左上已经全是 0,只剩右下一个 1。继续扫:(0,1)、(1,0) 已经是水,不再加 count。扫到 (2,2),又是一块没见过的陆地,count 变成 2,四邻都是水或越界,单格岛淹完。再往下没有格子,返回 2。
count +1 的时机必须是「发现新岛的起点」,不能是「每走进一个陆地格子」。走进格子是遍历内部的事。对角线也不连:邻居公式只有 (r±1,c) 和 (r,c±1),写错就改了图。每个格子进出常数次,时间 O(m·n);递归最深时整张图都是陆地,空间 O(m·n)。原地置 0 省掉 visited 数组,语义仍是「来过」。
淹没法「发现即淹没」把计数和遍历拆开:外层只在新岛起点 +1,内层负责把连通块烧干净。没有额外去重结构,每座岛也只会被数一次。
Go:DFS 淹没
func numIslands(grid [][]byte) int {if len(grid) == 0 { return 0 }m, n := len(grid), len(grid[0])count := 0var sink func(int, int)sink = func(i, j int) {if i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0' { return }grid[i][j] = '0'sink(i+1, j); sink(i-1, j); sink(i, j+1); sink(i, j-1)}for i := 0; i < m; i++ {for j := 0; j < n; j++ {if grid[i][j] == '1' { count++; sink(i, j) }}}return count}
1空网格直接返回 0。sink 先问越界或已经是水:没有这条边,立刻返回,再读格子也不会越界。
2走进陆地立刻原地置 '0',这就是访问标记。不先标记再递归,四连通的环会把你转回来。
3四个方向各钻一次。顺序不影响岛屿数,因为问的是连通块集合,不是层数。
4外层双循环只在仍是 '1' 时先 count++ 再 sink:加一的是新岛起点,不是每一个被走进的格子。
总结
看见还露着的陆地就 +1,再 DFS 把四连通的整座岛淹成水。
- 数的是连通块,不是陆地格子。不淹没就重复计数;不标记就会在岛上转圈。
- 网格是隐式图:邻居按坐标加减现算,越界或水当作没边。原地置 0 与 visited 数组同义。
- LC695 求最大面积,只需在同一次淹没里累加格子数;骨架仍是「发现起点 + 走完连通块」。