leetcode 中有一系列的岛屿类问题,这一类问题都可以使用 DFS(深度优先搜索) 方法来解决。我们所熟悉的 DFS 问题通常是在树或者图结构上进行的,而岛屿类 DFS 问题是在网格结构中进行的。岛屿类问题是网格 DFS 问题的典型代表,本文以岛屿类问题为例,展示网格类 DFS 问题的通用解题思路。
网格结构:由 m × n m \times n m×n 个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,我们就是在这样的网格上进行搜索。
岛屿问题中的网格:每个格子中的数字可能是 0 或者 1。把数字为 0 的格子看成海洋,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿。
网格结构要比二叉树结构复杂一些。我们先写出二叉树的 DFS 遍历方法,再类比写出网格结构的 DFS 遍历方法。
二叉树 DFS 遍历一般是这样的:
public void traverse(TreeNode root){
// 递归结束条件
if(root == null)
return;
// 递归遍历两个相邻的节点:左孩子、右孩子
TreeNode(root.left);
TreeNode(root.right);
}
可以看到,二叉树的 DFS 有两个要素:访问相邻节点和递归结束条件。
类比二叉树,我们分析网格问题的 DFS 遍历方法,先写出两个要素:
据此,我们可以写出网格 DFS 遍历的框架代码:
public void dfs(int[][] grid, int r, int c){
// 递归结束条件:如果坐标(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);// 右
}
public boolean inArea(int[][] grid, int r, int c){
return r >= 0 && r < grid.length && c >= 0 && c < grid[0].length;
}
二叉树的 DFS 每次搜索左右孩子,可以看作是向下搜索,所以不会重复搜索。而网格结构的 DFS 每次向上、下、左、右四个方向搜索,自然会有重复遍历的节点。通过标记已经遍历过的格子,就能避免重复遍历。
以岛屿问题为例,0 代表海洋,1 代表陆地,我们把遍历过的格子置 2,这样在搜索遍历时,先判断当前格子,如果值为 2,说明已经遍历过,就可以跳过这个格子。
在上面的框架代码中加入避免重复的语句:
public void dfs(int[][] grid, int r, int c){
// 递归结束条件:如果坐标(r, c)超出网格,直接返回
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);// 右
}
public boolean inArea(int[][] grid, int r, int c){
return r >= 0 && r < grid.length && c >= 0 && c < grid[0].length;
}
这样,我们得到了一个网格 DFS 问题的框架代码,再结合具体问题进行修改,就能解决一般的岛屿类问题以及网格类问题。
题目描述:
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
思路:
从网格的左上角开始进行 DFS。走到一个格子,若该格子不是陆地,则跳过该格子继续搜索;若该格子是陆地,就往它的上下左右四个方向进行搜索,直至边界处或者遇到已经遍历过的格子,一个岛屿成功找出,岛屿数量 count + 1。
参考代码:
class Solution {
public 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'){
dfs(grid, i, j);
count++;
}
}
}
return count;
}
public void dfs(char[][] grid, int r, int c){
// 递归结束条件:如果坐标(r, c)超出网格,直接返回
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);// 右
}
public boolean inArea(char[][] grid, int r, int c){
return r >= 0 && r < grid.length && c >= 0 && c < grid[0].length;
}
}
熟悉之后,可以把递归结束条件(包括判断格子是否在网格中以及该格子是否遍历过)写在一个 if 语句中,而不用单独写一个 inArea() 方法。参考代码如下:
class Solution {
public 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'){
dfs(grid, i, j);
count++;
}
}
}
return count;
}
public void dfs(char[][] grid, int r, int c){
// 递归结束条件
if(r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || 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);// 右
}
}
题目描述:
给定一个包含了一些 0 和 1 的非空二维数组 grid 。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
示例 1:
[[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:
[[0,0,0,0,0,0,0,0]]
对于上面这个给定的矩阵, 返回 0。
思路:
上一道题只是找出了岛屿的个数,这道题相当于在上一道题的基础上还要求出每一个岛屿的面积,最后返回最大的面积。对于一个陆地格子 ( i , j ) (i,j) (i,j),由它向四个方向搜索找到的岛屿的面积就等于 1 ( 它 本 身 ) + 四 个 方 向 搜 索 的 岛 屿 面 积 1(它本身) + 四个方向搜索的岛屿面积 1(它本身)+四个方向搜索的岛屿面积 即
1 + area(grid, r - 1, c)
+ area(grid, r + 1, c)
+ area(grid, r, c - 1)
+ area(grid, r, c + 1);
参考代码:
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int maxArea = 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
int t = area(grid, i, j);
maxArea = Math.max(maxArea,t);
}
}
return maxArea;
}
public int area(int[][] grid, int r, int c){
// 递归结束条件
if(r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || grid[r][c] != 1)
return 0;
grid[r][c] = 2;
return 1
+ area(grid, r - 1, c)
+ area(grid, r + 1, c)
+ area(grid, r, c - 1)
+ area(grid, r, c + 1);
}
}
题目描述:
给定一个包含 0 和 1 的二维网格地图,其中 1 表示陆地 0 表示水域。
网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
由题意,岛屿的周长就是计算岛屿全部的边的和,而这些边就是我们在 DFS 遍历中,dfs 函数返回的位置(递归结束条件)。我们可以将岛屿的周长中的边分为两类,如下图所示:黄色的边是与网格边界相邻的周长,蓝色的边是与海洋格子相邻的周长。
当 dfs 函数因为坐标 (r, c) 超出网格范围而返回的时候,实际上岛屿就经过了一条黄色的边;当函数因为当前格子是海洋格子而返回的时候,实际上就经过了一条蓝色的边。
参考代码:
class Solution {
public 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 r, int c){
// 坐标 (r, c) 超出网格范围时返回,岛屿经过一条黄色的边
if(r < 0 || r >= grid.length || c < 0 || c >= grid[0].length)
return 1;
// 当前格子是海洋格子时返回,岛屿经过一条蓝色的边
if(grid[r][c] == 0)
return 1;
// 已经遍历过的格子
if(grid[r][c] == 2)
return 0;
grid[r][c] = 2;
return dfs(grid, r - 1, c)
+ dfs(grid, r + 1, c)
+ dfs(grid, r, c - 1)
+ dfs(grid, r, c + 1);
}
}
参考自:
岛屿类问题的通用解法、DFS 遍历框架