在 LeetCode 中,「岛屿问题」是一个系列系列问题,比如:

  • 200. 岛屿数量 (Medium)
  • 463. 岛屿的周长 (Easy)
  • 695. 岛屿的最大面积 (Medium)
  • 827. 最大人工岛 (Hard)

我们所熟悉的 DFS(深度优先搜索)问题通常是在树或者图结构上进行的。而我们今天要讨论的 DFS 问题,是在一种「网格」结构中进行的。岛屿问题是这类网格 DFS 问题的典型代表。网格结构遍历起来要比二叉树复杂一些,如果没有掌握一定的方法,DFS 代码容易写得冗长繁杂。

本文将以岛屿问题为例,展示网格类问题 DFS 通用思路,以及如何让代码变得简洁。

一. 网格类问题的 DFS 遍历方法

1.网格问题的基本概念

我们首先明确一下岛屿问题中的网格结构是如何定义的,以方便我们后面的讨论。

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

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

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

2.DFS 的基本结构

网格结构要比二叉树结构稍微复杂一些,它其实是一种简化版的图结构。要写好网格上的 DFS 遍历,我们首先要理解二叉树上的 DFS 遍历方法,再类比写出网格结构上的 DFS 遍历。我们写的二叉树 DFS 遍历一般是这样的:

void traverse(TreeNode root) {// 判断 base caseif (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] 会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。

这样,我们得到了网格 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;
}

3.如何避免重复遍历

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

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

  • 0 —— 海洋格子
  • 1 —— 陆地格子(未遍历过)
  • 2 —— 陆地格子(已遍历过)

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

二.Leetcode题目解答

1. Leetcode 200题:岛屿数量

1.1 题目描述 

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例1:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-islands

 1.2 解答

class Solution {//DFSpublic int numIslands(char[][] grid) {//记录岛屿数量int count = 0;//循环遍历for(int i = 0; i < grid.length; i++){for(int j = 0; j < grid[0].length; j++){if(grid[i][j] == '1'){//如果等于1,开始dfs,找到岛屿的边界dfs(grid, i, j);count++;}}}return count;}//辅助方法:找到岛屿的边界,并把当前岛屿置0,避免重复计算public void dfs(char[][] grid, int i, int j){if(i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] != '1'){return;}//将当前位置置0,避免重复计算grid[i][j] = '2';//搜索上下左右dfs(grid, i + 1, j);dfs(grid, i - 1, j);dfs(grid, i, j + 1);dfs(grid, i, j - 1);}
}

2. Leetcode 695题:岛屿的最大面积

2.1 题目描述

给你一个大小为 m x n 的二进制矩阵 grid 。

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

示例1:

输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/max-area-of-island

 2.2 解答

class Solution {//DFSpublic int maxAreaOfIsland(int[][] 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){//如果等于1,开始dfs,计算岛屿的面积int area = dfs(grid, i, j);res = Math.max(res, area);}}}return res;}//辅助方法:计算岛屿的面积public int dfs(int[][] grid, int i, int j){if(i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] != 1){return 0;}//将当前位置置0,避免重复计算grid[i][j] = 2;//搜索上下左右return 1 + dfs(grid, i + 1, j) + dfs(grid, i - 1, j) + dfs(grid, i, j + 1) + dfs(grid, i, j - 1);}
}

3. Leetcode 463题:岛屿的周长

3.1 题目描述 

给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。

网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。

岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

示例1:

输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
输出:16
解释:它的周长是上面图片中的 16 个黄色的边

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/island-perimeter

3.2 解答

这道题稍微不太一样。

岛屿的周长是计算岛屿全部的「边缘」,而这些边缘就是我们在 DFS 遍历中,dfs 函数返回的位置。观察题目示例,我们可以将岛屿的周长中的边分为两类,如下图所示。黄色的边是与网格边界相邻的周长,而蓝色的边是与海洋格子相邻的周长。

当我们的 dfs 函数因为「坐标 (r, c) 超出网格范围」返回的时候,实际上就经过了一条黄色的边;而当函数因为「当前格子是海洋格子」返回的时候,实际上就经过了一条蓝色的边。这样,我们就把岛屿的周长跟 DFS 遍历联系起来了。

完整代码如下:

class Solution {//DFSpublic int islandPerimeter(int[][] grid) {//循环遍历for(int i = 0; i < grid.length; i++){for(int j = 0; j < grid[0].length; j++){if(grid[i][j] == 1){//题目说只有一个岛屿,找到一个就行return dfs(grid, i, j);}}}return 0;}//辅助方法:计算岛屿的周长public int dfs(int[][] grid, int i, int j){//如果超出边界,返回一条和边界相连的边if(i < 0 || j < 0 || i >= grid.length || j >= grid[0].length){return 1;}//如果遇到海洋,返回一条和海洋相连的边if(grid[i][j] == 0){return 1;}//2代表当前格子已经遍历过,直接返回if(grid[i][j] == 2){return 0;}//将当前位置置2,避免重复计算grid[i][j] = 2;//搜索上下左右return dfs(grid, i + 1, j) + dfs(grid, i - 1, j) + dfs(grid, i, j + 1) + dfs(grid, i, j - 1);}
}

Leetcode岛屿问题系列分析相关推荐

  1. c# 两个list比较_C#刷遍Leetcode面试题系列连载(1) 入门与工具简介(VS Code amp; VS)...

    什么要刷LeetCode 大家都知道,很多对算法要求高一点的软件公司,比如美国的FLAGM (Facebook.LinkedIn.Amazon/Apple.Google.Microsoft),或国内大 ...

  2. C#刷遍Leetcode面试题系列连载(6):No.372 - 超级次方

    点击蓝字"dotNET匠人"关注我哟 加个"星标★",每日 7:15,好文必达! 前文传送门: C# 刷遍 Leetcode 面试题系列连载(1) - 入门与工 ...

  3. C#刷遍Leetcode面试题系列连载(1) - 入门与工具简介

    点击蓝字"dotNET匠人"关注我哟 加个"星标★",每日 7:15,好文必达! 什么要刷LeetCode 大家都知道,很多对算法要求高一点的软件公司,比如美国 ...

  4. 八十四、Python | Leetcode回溯算法系列

    @Author:Runsen @Date:2020/7/7 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  5. 八十二、Python | Leetcode贪心算法系列

    @Author:Runsen @Date:2020/7/5 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  6. python leetcode_八十二、Python | Leetcode贪心算法系列

    @Author:Runsen @Date:2020/7/5 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  7. Leetcode滑窗系列(java):643. 子数组最大平均数 I

    Leetcode滑窗系列(java):643. 子数组最大平均数 I(新手小白仅供参考) 题目来源 leetcode 题目描述 个人思路 创建一个滑窗,将其值的和作为作为判断基准 然后滑窗的左右边界各 ...

  8. LeetCode动态规划股票系列整理

    写在前面 股票感觉是LeetCode动态规划中系列最多的一类,交易次数不同,有冷冻期,含手续费,让买卖的最佳时机千奇百怪,但是只要掌握dp的方法,解决起来还是有套路可循的.依据dp的常规思想,股票问题 ...

  9. 漏洞复现-webmin漏洞系列分析与利用

    webmin漏洞系列分析与利用 Webmin 远程代码执行CVE-2022-0824 编号CVE编号:CVE-2022-0824 影响版本 漏洞简介 复现 拉取漏洞镜像 启动漏洞环境 解决方案 POC ...

最新文章

  1. java -- 线程的生命周期
  2. Docker之 默认桥接网络与自定义桥接网卡
  3. 腾讯qq在线状态,开放平台
  4. 线下课程推荐 | 知识图谱理论与实战:构建行业知识图谱 (第四期)
  5. subpage新写法
  6. SAP C4C的URL Mashup无法添加到embedded component里去
  7. 《Effective C#》读书笔记-1.C# 语言习惯-2.使用运行时常量(readonly)而不是编译时常量(const)...
  8. WAI-ARIA对自动完成小部件的支持
  9. 苹果付费app共享公众号_娄底共享云店铺公众号
  10. Openstack Python 源代码的路径
  11. android服务常驻后台,android-如何始终在后台运行服务?
  12. .NET反编译工具Reflector及插件Reflector.FileDisassembler.dll
  13. 实战CSS:模拟登录注册静态实现
  14. 用Ps按比例缩小图片整体的尺寸
  15. jQuery 选择器
  16. 懒羊羊的作业:看过国产动画片的同学都知道,懒羊羊是一只非常懒的羊,整天除了吃就是睡,根本没有时间做作业。明天就是周一了,村长慢羊羊留的作业:把 n 个整数从大到小排序,它还没开始写...
  17. (一)、写一个怪物的类,类中有属性姓名(name),攻击力(attack),有打人的方法(fight)。(方法的重写)...
  18. 关于android的nfc问题 Ultralight c (通用卡)
  19. linux mysql dengl_mysql中类似oracle的over分组实现
  20. 开源情报分析(OSINT)CTF社工类2万字题详细教程,请不要利用本文章做不道德的事,后果概不负责

热门文章

  1. 如何将无线键盘连接到Mac?
  2. 新浪微博分享接口|利用jiathis自定义接口
  3. nike 2015 bccz icdu mtdf
  4. mt7620a上带机量的提高(一)
  5. 暴风魔镜VR(第一人称和第三人称)
  6. 计算机快捷键大全windows,windows系统常见快捷键大全
  7. 『矩阵论笔记』线性判别分析(LDA)最全解读+python实战二分类代码+补充:矩阵求导可以参考
  8. Web前端教程分享:页面性能优化办法有哪些?
  9. 转 CRT 上传 下载
  10. android面试题-人事面试宝典二