Skip to content

题目链接 · 灵神原题解(署名来源)

先说总体思路,然后说 DFS 的细节。

总体思路

看示例 2:

假设你是哥伦布。先从左上角开始,把第一个岛全部插上旗子🚩,这里用 2 表示。插满旗子后,把答案(岛屿个数)加一。

注意:在岛上,你只能左右上下走,不能斜方向走。

继续遍历,寻找其他的岛屿,也就是 1。找到 1 意味着发现了一个新的岛,继续插上旗子🚩,把答案加一。

继续遍历,寻找其他的岛屿。找到 1 意味着发现了一个新的岛,插满旗子🚩,把答案加一。

如果没有 1 了,算法就结束了,返回答案(岛屿个数)。

如何实现?

一旦我们发现 (i,j)1,就从 (i,j) 开始,DFS 这个岛。

每一步可以往左右上下四个方向走,也就是

(i,j1),(i,j+1),(i1,j),(i+1,j)

这四个格子。

如果到达一块未发现的陆地格子,就插上旗子🚩,把 grid[i][j] 改成 2

如果 (i,j) 出界,或者 (i,j) 是水,或者 (i,j) 已发现(已插上旗子🚩),就不再继续往下递归。

注意:DFS 的过程中,最重要的是不能重复访问之前访问过的格子

比如从左上角 (0,0) 向右移动到 (0,1),然后从 (0,1) 又向左移动到 (0,0),再从 (0,0) 向右移动到 (0,1),如此往复,就无限递归下去了。

怎么避免重复访问?本题的做法是把访问过的格子都插上旗子🚩。例如从 (0,1) 往左走,发现 (0,0) 是插过旗子的格子,就不继续走了。

答疑

:二叉树的递归和网格图的递归有何区别?

:列表总结如下。

二叉树网格图
递归入口根节点岛屿第一行最左边的陆地
递归方向左儿子和右儿子左右上下的相邻陆地
递归边界空节点(或者叶节点)出界、遇到水或者旗子

:如何理解递归?

:递归的思想是,假设你是一家公司的老板,你不需要万事亲力亲为,而是拆解问题,交给下属去做。对于这题来说,就是从岛屿的某个位置登陆插旗,然后一分为四,把探索整个岛屿的任务交给其他人去处理,自己只需处理好第一步就行。

:我可以先判断 grid[i][j] 是否等于 1,再判断 (i,j) 是否出界吗?

:不行,比如 i=1,获取 grid[i][j] 就下标越界了。要先保证下标在范围中,再去获取数组元素值。

:我可以把访问过的 grid[i][j] 改成 0 吗?

:可以。相当于把访问过的位置变成水。其实,改成除了 1 以外的任何值都行。

:我可以把这行代码 grid[i][j] = '2' 写在 dfs 的最后一行吗?

:不行。这样写会先执行递归,比如从左上角 (0,0) 向右移动到 (0,1),内部继续递归,从 (0,1) 又向左移动到 (0,0),导致反复横跳,无限递归。

:DFS 中能否只考虑往右走和往下走?

:不行,比如蚊香形的岛屿,在探索的时候必须具备左右上下走的能力。

python
class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        m, n = len(grid), len(grid[0])

        def dfs(i: int, j: int) -> None:
            # 出界,或者不是 '1',就不再往下递归
            if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
                return
            grid[i][j] = '2'  # 插旗!避免来回横跳无限递归
            dfs(i, j - 1)  # 往左走
            dfs(i, j + 1)  # 往右走
            dfs(i - 1, j)  # 往上走
            dfs(i + 1, j)  # 往下走

        ans = 0
        for i, row in enumerate(grid):
            for j, c in enumerate(row):
                if c == '1':  # 找到了一个新的岛
                    dfs(i, j)  # 把这个岛插满旗子,这样后面遍历到的 '1' 一定是新的岛
                    ans += 1
        return ans
cpp
// C++ 版待补充
cpp
class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size(), n = grid[0].size();
        auto dfs = [&](this auto&& dfs, int i, int j) -> void {
            // 出界,或者不是 '1',就不再往下递归
            if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != '1') {
                return;
            }
            grid[i][j] = '2'; // 插旗!避免来回横跳无限递归
            dfs(i, j - 1); // 往左走
            dfs(i, j + 1); // 往右走
            dfs(i - 1, j); // 往上走
            dfs(i + 1, j); // 往下走
        };

        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') { // 找到了一个新的岛
                    dfs(i, j); // 把这个岛插满旗子,这样后面遍历到的 '1' 一定是新的岛
                    ans++;
                }
            }
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(mn),其中 mn 分别是 grid 的行数和列数。由于 DFS 中修改了 grid[i][j] = '2',当我们访问一个访问过的格子时,会触发 if grid[i][j] != '1': return。只有首次访问一个格子时,才会继续递归,其余情况不会继续递归。每次插上一个旗子只需要 O(1) 的时间,插上至多 mn 个旗子,就需要 O(mn) 的时间。
  • 空间复杂度:O(mn)。最坏情况下,对于蛇形陆地、蚊香型陆地等,递归需要 O(mn) 的栈空间。

思考题

  1. 构造一个 grid,让上面代码的递归深度达到最大。
  2. 如果调整四个方向的递归顺序(比如顺序为右下左上),递归路径的形状会发生什么变化?

欢迎在评论区分享你的思路。

专题训练

见下面的网格图题单。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

本文整理自灵茶山艾府(endlesscheng)的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传