前言
我们知道,DFS
通常是在树或者图结构上进行的,而岛屿问题都是网格,能不能用DFS
呢?
可以,记住,凡是网格的都应该想到用DFS
,岛屿问题就是一类典型的网格问题。
一、岛屿数量(中)
题目
给你一个由 ‘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
示例 2:
输入:$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
的基本结构,先看简单的二叉树DFS
遍历结构
1 | void traverse(TreeNode* root) { |
可以看到,二叉树的DFS
有两个要素:访问相邻结点、判断base case
。
- 二叉树的相邻结点非常简单,只有左子结点和右子结点两个。
- 二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。那么我们的
DFS
遍历只需要递归调用左子树和右子树即可。
- 二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。那么我们的
- 二叉树遍历的
base case
是root == NULL
。- 这样一个条件判断其实有两个含义:一方面,这表示
root
指向的子树为空,不需要再往下遍历了。另一方面,在root == NULL
的时候及时返回,可以让后面的root->left
和root->right
操作不会出现空指针异常。
- 这样一个条件判断其实有两个含义:一方面,这表示
那么对于网格上的DFS
,我们完全可以参考二叉树的DFS
,写出网格DFS
的两个要素。
- 首先看相邻结点。很明显,网格结构中的格子的相邻结点是上下左右四个,即
(row-1, col),(row+1, col),(row, col-1),(row, col+1)
; - 然后是
base case
。根据二叉树的对应过来,是超出网格范围的格子,即row >= grid.size() || col >= grid[0].size() || row < 0 || col < 0
。
根据分析,可以得出网格DFS
遍历的框架代码:
1 | void dfsGrid(vector<vector<char>>& grid, int row, int col) { |
这里有个问题,怎么避免重复值,比如下面这张图,dfsGrid
遍历时会一直在这里不断循环。
简单的方法就是标记已经遍历过的格子。比如岛屿问题,把走过的陆地格子的值改为2。这样就能得到一个网格DFS
遍历的通用框架代码:
1 | void dfsGrid(vector<vector<char>>& grid, int row, int col) { |
有了网格DFS
遍历的通用框架,我们只需要用两层for循环遍历整张二维表格中所有的陆地,连续的视为一个岛屿。
1 | class Solution { |
二、岛屿的周长(简)
题目
给定一个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 个黄色的边
示例 2:
输入:grid = [[1]]
输出:4
示例 3:
输入:grid = [[1,0]]
输出:4
提示:
$row == grid.length$
$col == grid[i].length$
$1 <= row, col <= 100$
$grid[i][j] 为 0 或 1$
题解
这道题最牛逼的一点是你要想到,岛屿的周长就是岛屿方格和非岛屿方格相邻的边的数量(如下图所示)。也就是说,在DFS
遍历中,从一个岛屿方格走向一个非岛屿方格,就将周长加1。
所以,我们可以修改下网格DFS
遍历的通用框架:
1 | int dfsGrid(vector<vector<int>>& grid, int row, int col) { |
题目限制只有一个岛屿,那我们计算一个即可
1 | class Solution { |
三、岛屿的最大面积(中)
题目
给你一个大小为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 。
示例 2:
输入:$grid = [[0,0,0,0,0,0,0,0]]$
输出:0
提示:
$m == grid.length$
$n == grid[i].length$
$1 <= m, n <= 50$
$grid[i][j] 为 0 或 1$
题解
从上面两道我们已经知道怎么计算岛屿数量和一个岛屿的周长,这道题是结合了上面两道。
因此我们可以对每个岛屿计算它的面积,最后返回最大的那个面积即可。
1 | class Solution { |
四、最大人工岛(困)
题目
给你一个大小为n x n
二进制矩阵grid
。最多只能将一格0
变成1
。
返回执行此操作后,grid
中最大的岛屿面积是多少?
岛屿由一组上、下、左、右四个方向相连的1
形成。
示例 1:
输入: $grid = [[1, 0], [0, 1]]$
输出: 3
解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。
示例 2:
输入: $grid = [[1, 1], [1, 0]]$
输出: 4
解释: 将一格0变成1,岛屿的面积扩大为 4。
示例 3:
输入: $grid = [[1, 1], [1, 1]]$
输出: 4
解释: 没有0可以让我们变成1,面积依然为 4。
提示:
$n == grid.length$
$n == grid[i].length$
$1 <= n <= 500$
$grid[i][j] 为 0 或 1$
题解
这道题是第三题的升级版,现在我们可以将一个海洋变成陆地,从而连接两个岛屿。
那我们就需要先统计各个岛屿面积,找到最大的岛屿;然后把一个海洋变成陆地,再统计一遍连接后各个岛屿面积,找到最大的岛屿。
因此需要两次DFS
遍历:
1、划分岛屿,给每个岛屿标号
标号要标什么呢?假设我们在所有的格子上标记出岛屿的面积。然后搜索哪个海洋格子相邻的两个岛屿面积最大。
例如下图中红色方框内的海洋格子,上边、左边都与岛屿相邻,我们可以计算出它变成陆地之后可以连接成的岛屿面积为7 + 1 + 2 = 10
。
然而,这种做法可能遇到一个问题。如下图中红色方框内的海洋格子,它的上边、左边都与岛屿相邻,这时候连接成的岛屿面积难道是7 + 1 + 7 = 15
?显然不是。这两个7来自同一个岛屿,所以填海造陆之后得到的岛屿面积应该只有7 + 1 = 8
。
可以看到,要让算法正确,我们得能区分一个海洋格子相邻的两个7是不是来自同一个岛屿。那么,我们不能在方格中标记岛屿的面积,而应该用map
记录每个岛屿面积,给每个岛屿标记map
的key
。
如下图所示。这样我们就可以发现红色方框内的海洋格子,它的两个相邻的岛屿实际上是同一个。
2、填充海洋,连接四周的岛屿
和上面类似,要遍历每个海洋格子上下左右的格子。又因为我们已经有map
来记录了各个岛屿的面积,所以只需要在遍历时发现是岛屿,加上对应的面积即可,不需要再全部遍历该岛屿的陆地。
要注意的是,我们是将一个海洋变为陆地,所以海洋会占一个面积。
1 | class Solution { |