题目描述

✅ 面试题 16.19. 水域大小

image-20260929010351587

题意分析

矩阵中 0 表示水,非 0 表示陆地。上下、左右以及四条对角线都算相邻,因此连通规则是八方向。要找出每一片水域的格子数,并把所有大小按升序返回。

题目不是统计 0 的总数,而是按连通分量分组。若从每个 0 都重新搜索却不记录访问状态,同一片水域会被重复计算;若只走四方向,沿对角线接触的水会被错误拆开。

可以直接把已发现的 0 改成 1,复用输入矩阵作为访问标记。题目矩阵最多有 $1000\times1000$ 个格子,整片水域会让递归 DFS 形成过深的调用链,因此使用显式栈执行 DFS;这份实现会修改原矩阵。

解法:八方向 DFS 洪水填充

核心思路

[!blue]

扫描矩阵,遇到仍为 0 的格子就开始统计一片新水域。将起点标为 1 并压入栈,之后每次弹出一个水格,将大小加 1,再检查它周围的八个方向。合法且仍为 0 的邻居立刻标记并压栈,等待后续处理。

标记必须发生在压栈时,不能等弹出后才做,否则同一个水格可能被多个邻居重复压入。所有入栈格子都能从起点沿水格到达;反过来,任一与起点连通的水格,都可以沿某条相邻路径被逐步发现。因此栈为空时,恰好处理完这一片水域,每个水格只贡献一次计数。

栈中用 row * n + col 保存坐标,弹出后通过除以 n、对 n 取余还原行列。这样只保存一个整数即可表示一个格子,最多百万格的下标也在整数范围内。邻居循环枚举周围九个位置,其中中心格已被标记,会自然跳过,实际仍只扩展八方向。

一次搜索会把整片水域全部标记,后续行列扫描不会再次启动同一片水域。搜索得到的大小按发现位置排列,与大小无关,所以最后还要统一升序排序。全为陆地时不启动搜索,直接返回空结果。

解题步骤

  1. 记录行列数,逐格扫描,遇到 0 时启动一次 DFS。
  2. 将起点编码入栈并立即标记,水域大小从 0 开始。
  3. 弹出一个格子并计数,检查周围八个邻居;对合法的未发现水格先标记再压栈。
  4. 栈空时返回这片水域的大小,加入答案后继续行列扫描。
  5. 将所有水域大小升序排序并返回。

代码实现

class Solution {
    private int m;
    private int n;
    private int[][] land;

    public int[] pondSizes(int[][] land) {
        m = land.length;
        n = land[0].length;
        this.land = land;
        List<Integer> answer = new ArrayList<>();

        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (land[i][j] == 0) {
                    answer.add(dfs(i, j));
                }
            }
        }

        return answer.stream().sorted().mapToInt(Integer::valueOf).toArray();
    }

    private int dfs(int i, int j) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(i * n + j);
        land[i][j] = 1;
        int res = 0;

        while (!stack.isEmpty()) {
            int cell = stack.pop();
            int row = cell / n;
            int col = cell % n;
            res++;

            for (int x = row - 1; x <= row + 1; ++x) {
                for (int y = col - 1; y <= col + 1; ++y) {
                    if (x >= 0 && x < m && y >= 0 && y < n && land[x][y] == 0) {
                        land[x][y] = 1;
                        stack.push(x * n + y);
                    }
                }
            }
        }

        return res;
    }
}
import (
    "sort"
)

func pondSizes(land [][]int) (answer []int) {
    m, n := len(land), len(land[0])
    dfs := func(i, j int) int {
        stack := []int{
            i*n + j,
        }
        land[i][j] = 1
        res := 0
        for len(stack) > 0 {
            cell := stack[len(stack)-1]
            stack = stack[:len(stack)-1]
            row, col := cell/n, cell%n
            res++
            for x := row - 1; x <= row+1; x++ {
                for y := col - 1; y <= col+1; y++ {
                    if x >= 0 && x < m && y >= 0 && y < n && land[x][y] == 0 {
                        land[x][y] = 1
                        stack = append(stack, x*n+y)
                    }
                }
            }
        }
        return res
    }
    for i := range land {
        for j := range land[i] {
            if land[i][j] == 0 {
                answer = append(answer, dfs(i, j))
            }
        }
    }
    sort.Ints(answer)
    return
}

复杂度分析

  • 时间复杂度:$O(mn+p\log(p+1))$,其中 $p$ 为水域数量。每个水格只入栈、出栈一次,每次检查常数个邻居,最后排序全部水域大小。
  • 空间复杂度:$O(mn)$,用于最坏情况下的显式栈及结果列表。访问标记复用原矩阵,不再依赖递归调用栈。

关键点总结

[!green]

  • 连通方向是八个,不是常见岛屿题的四个;读题时要先确认邻接定义。
  • 发现水格时立刻标记,维持“每个水格只入栈一次”的不变量。
  • 每次搜索独立计数并返回连通块大小,不让不同水域的统计混在一起。
  • 显式栈保存待处理格子,避免大水域导致调用栈过深。

易错点总结

[!yellow]

  • 只枚举上下左右:仅对角相连的水格会被错误拆成不同水域。
  • 弹出后才标记访问:一个水格可能被多个邻居重复压栈,造成重复计数。
  • 每次 DFS 使用同一个全局计数却不清零:后一个水域会累加前一个水域的大小。
  • 忘记排序:发现顺序由矩阵位置决定,不能保证水域大小升序。
  • 忽略副作用:当前实现把水改成 1;调用后再次使用原矩阵会看到被修改的数据。

相似题目

题目 难度 关联与区别
695. 岛屿的最大面积 中等 同样统计连通块面积,本题水为0且使用八方向,原题使用四方向。
200. 岛屿数量 中等 从统计连通块个数扩展为返回每块大小,访问标记方法相同。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/57383910
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!