前言
深度优先搜索DFS算法通常出现在树结构或者图结构上,上一篇文章我们已经讲了二叉树的深度优先遍历,本文我们将二叉树的深度遍历继续引申,拓展到网格型深度优先遍历。
首先我们先来统一一下网格的概念,网格是由m * n个小方格组成的网状结构,每个小方格与其上下左右4个方格都是相邻的,题目要求在这样的网格中进行某种搜索。
本文借leetcode上的几个岛屿问题来讲解DFS,一般地,岛屿问题题目中每个格子中的数字是0或1。数字0代表海水,1代表陆地,相邻的陆地就连成了一个岛屿。
在这个前提下,就会衍生出各种岛屿问题,包括岛屿的数量,面积等。这些问题都能用DFS算法模板解决。
正文
1、DFS算法模板
网格是一种简化的图结构,要写好网格DFS,我们先来回顾一下二叉树的DFS是怎么写的。
void traverse(TreeNode root) {
// base case
if (root == null) {
return;
}
// 访问两个相邻结点:左子结点、右子结点
traverse(root.left);
traverse(root.right);
}
二叉树的DFS遍历有2大步骤:判断最小规模base case和访问相邻节点。
第一要素:Base Case。一般地,二叉树的base case是root == null,也就是问题的最小规模是一颗空树,同时也代表了当root指向的树为空时,不再需要往下遍历了。
第二要素:访问相邻节点。二叉树访问相邻节点非常简单,因为节点半身就定义一个指针指向自己的左右孩子,只需要root.left和root.right即可访问相邻节点。
那么,参考二叉树的2个要素,我们来写出DFS的两个要素:
第一要素:Base Case。应该是网格中不能再继续递归的条件,那就是超过了格子的界限。
第二要素:访问相邻节点。网格中的格子有几个相邻节点?答案是4个,对于格子(i,j)来说,4个相邻的格子分别是(i+1,j), (i-1,j),(i,j+1),(i,j-1)。其实和二叉树没什么区别,可以把网格看成是四叉树。
至此,我们可以写出DFS算法的框架:
void dfs(int[][] grid, int i, int j) {
// 判断 base case
// 如果坐标 (i, j) 超出了网格范围,直接返回
int m = grid.length;
int n = grid[0].length;
if (i < 0 || i >= m || j < 0 || j >= n) {
return;
}
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
但是,还有一个问题,网格类的DFS不同于二叉树DFS,遍历时有可能遇到已经遍历过的点,如果不进行处理,就会出现不停地兜圈子,永远停不下来。
如何解决呢?答案就是标记已经遍历过的格子,下次再遇到的话就不再遍历了。于是我们加上解决重复访问的代码,最终的网格类DFS模板就是:
void dfs(int[][] grid, int i, int j) {
// 判断 base case
// 如果坐标 (i, j) 超出了网格范围,直接返回
int m = grid.length;
int n = grid[0].length;
if (i < 0 || i >= m || j < 0 || j >= n) {
return;
}
// 如果这个格子不是岛屿,直接返回
if (grid[i][j] != 1) {
return;
}
grid[i][j] = 2; // 将格子标记为「已遍历过」,或者别的值都可以,只要和0或1不同就行
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
2、LeetCode No. 200 岛屿数量
给你一个由 '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
这道题是非常经典的DFS题目,可快速按照上面的模板写出算法:
public int numIslands(char[][] grid) {
if (grid.length == 0) {
return 0;
}
int ret = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
// 如果发现一个格子是陆地,则遇到一个岛屿,数量加一
if (grid[i][j] == '1') {
ret++;
// 然后使用DFS将与这个格子相邻的格子置为“已访问”
dfs(grid, i , j);
}
}
}
return ret;
}
// 从(i,j)开始,将与之相邻的“陆地”置为“已访问”
void dfs(char[][] grid, int i, int j) {
// 超过边界即返回
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) {
return;
}
// 如果不是陆地,直接返回
if (grid[i][j] != '1') {
return;
}
// 将该格子置为“已访问”
grid[i][j] = '2';
// 递归访问上下左右4个方向的格子
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
3、LeetCode No. 1254 封闭岛屿的数量
这道题跟上一道题的区别在于,上一题规定二维数组的四周可以认为是被海水包围,所以靠近边界的陆地也算岛屿。而这道题规定上下左右全部被海水(用1表示,跟上题相反)包围的陆地才算作是岛屿,即题目中所说的封闭岛屿。
那么如何求封闭岛屿呢,只要将上一题的所有岛屿,减去靠边界的岛屿即可。代码如下:
public int closedIsland(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
// 先将靠近边界的岛屿DFS置为“已访问”
for (int i = 0; i < m; i++) {
dfs(grid, i, 0);
dfs(grid, i, n -1);
}
for (int j = 0; j < n; j++) {
dfs(grid, 0, j);
dfs(grid, m - 1, j);
}
// 再进行岛屿统计
int ret = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) {
ret++;
dfs(grid, i, j);
}
}
}
return ret;
}
// dfs算法同上
void dfs(int[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) {
return;
}
if (grid[i][j] != 0) {
return;
}
grid[i][j] = 2;
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}