一刷177-力扣热题-200岛屿数量(m)

题目:
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
-------------
示例:
输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j] 的值为 '0''1'
------------------
思考:
网格类问题的 DFS 遍历方法
网格问题的基本概念
我们首先明确一下岛屿问题中的网格结构是如何定义的,以方便我们后面的讨论。

网格问题是由 m \times nm×n 个小方格组成一个网格,
每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。

岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。
我们把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,
这样相邻的陆地格子就连接成一个岛屿。

在这里插入图片描述

在这样一个设定下,就出现了各种岛屿问题的变种,
包括岛屿的数量、面积、周长等。不过这些问题,基本都可以用 DFS 遍历来解决。

DFS 的基本结构
网格结构要比二叉树结构稍微复杂一些,它其实是一种简化版的图结构。
要写好网格上的 DFS 遍历,我们首先要理解二叉树上的 DFS 遍历方法,
再类比写出网格结构上的 DFS 遍历。我们写的二叉树 DFS 遍历一般是这样的:
void traverse(TreeNode root) {
    // 判断 base case
    if (root == null) {
        return;
    }
    // 访问两个相邻结点:左子结点、右子结点
    traverse(root.left);
    traverse(root.right);
}

可以看到,二叉树的 DFS 有两个要素:「访问相邻结点」和「判断 base case」。

第一个要素是访问相邻结点。二叉树的相邻结点非常简单,只有左子结点和右子结点两个。
二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。
那么我们的 DFS 遍历只需要递归调用左子树和右子树即可。

第二个要素是 判断 base case。
一般来说,二叉树遍历的 base case 是 root == null。
这样一个条件判断其实有两个含义:一方面,这表示 root 指向的子树为空,不需要再往下遍历了。
另一方面,在 root == null 的时候及时返回,
可以让后面的 root.left 和 root.right 操作不会出现空指针异常。

对于网格上的 DFS,我们完全可以参考二叉树的 DFS,写出网格 DFS 的两个要素:
首先,网格结构中的格子有多少相邻结点?答案是上下左右四个。
对于格子 (r, c) 来说(r 和 c 分别代表行坐标和列坐标),
四个相邻的格子分别是 (r-1, c)(r+1, c)(r, c-1)(r, c+1)。换句话说,网格结构是「四叉」的。

在这里插入图片描述

其次,网格 DFS 中的 base case 是什么?从二叉树的 base case 对应过来,
应该是网格中不需要继续遍历、grid[r][c] 会出现数组下标越界异常的格子,
也就是那些超出网格范围的格子。

在这里插入图片描述

这一点稍微有些反直觉,坐标竟然可以临时超出网格的范围?
这种方法我称为「先污染后治理」—— 甭管当前是在哪个格子,先往四个方向走一步再说,
如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,
发现 root == null 再返回。

这样,我们得到了网格 DFS 遍历的框架代码:
void dfs(int[][] grid, int r, int c) {
    // 判断 base case
    // 如果坐标 (r, c) 超出了网格范围,直接返回
    if (!inArea(grid, r, c)) {
        return;
    }
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

如何避免重复遍历
网格结构的 DFS 与二叉树的 DFS 最大的不同之处在于,遍历中可能遇到遍历过的结点。
这是因为,网格结构本质上是一个「图」,我们可以把每个格子看成图中的结点,
每个结点有向上下左右的四条边。在图中遍历时,自然可能遇到重复遍历结点。

这时候,DFS 可能会不停地「兜圈子」,永远停不下来,如下图所示:

在这里插入图片描述

如何避免这样的重复遍历呢?答案是标记已经遍历过的格子。
以岛屿问题为例,我们需要在所有值为 1 的陆地格子上做 DFS 遍历。
每走过一个陆地格子,就把格子的值改为 2,这样当我们遇到 2 的时候,就知道这是遍历过的格子了。
也就是说,每个格子可能取三个值:

0 —— 海洋格子
1 —— 陆地格子(未遍历过)
2 —— 陆地格子(已遍历过)
我们在框架代码中加入避免重复遍历的语句
void dfs(int[][] grid, int r, int c) {
    // 判断 base case
    if (!inArea(grid, r, c)) {
        return;
    }
    // 如果这个格子不是岛屿,直接返回
    if (grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 将格子标记为「已遍历过」
    
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

在这里插入图片描述

这样,我们就得到了一个岛屿问题、乃至各种网格问题的通用 DFS 遍历方法。
在一些题解中,可能会把「已遍历过的陆地格子」标记为和海洋格子一样的 0,美其名曰「陆地沉没方法」,
即遍历完一个陆地格子就让陆地「沉没」为海洋。这种方法看似很巧妙,
但实际上有很大隐患,因为这样我们就无法区分「海洋格子」和「已遍历过的陆地格子」了。
如果题目更复杂一点,这很容易出 bug。

class Solution {
    public int numIslands(char[][] grid) {
        int res = 0;
        for (int i = 0; i < grid.length; i ++) {
            for (int j = 0; j < grid[0].length; j ++) {
                if (grid[i][j] == '1') {
                    dfsGrid(grid, i, j);
                    res++;
                }
            }
        }
        return res;
    }
    private void dfsGrid(char[][] grid, int row, int col) {
        if (row >= grid.length || col >= grid[0].length || row < 0 || col < 0) {//边缘性检测
            return;
        }
        if (grid[row][col] != '1') return;//如果这个格子不是岛屿,直接返回
        grid[row][col] = '2'; //将格子标记为 已遍历过
        dfsGrid(grid, row - 1, col);
        dfsGrid(grid, row + 1, col);
        dfsGrid(grid, row, col - 1);
        dfsGrid(grid, row, col + 1);
    }
}

LC

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值