0.前言
本文我们来学习一下算法题中颇为著名的岛屿数量问题,我将会从问题本身入手,详细分析解题思路,给出完整代码并进行解析,最后简单了解一下几个岛屿问题的变种题目。
1. 问题描述
题目给出一个只含有 0 和 1 矩阵,1 代表是陆地,0 代表海洋, 如果两个 1 相邻,那么这两个 1 属于 同一个岛,我们暂时只考虑 上下左右为相邻。
上下左右相邻的陆地可以组成一个岛屿,我们的目标是计算 岛屿的个数。
当输入为下图的矩阵时,共有三个岛屿,因此输出应该为 3 。
2. 解题思路
当题目给出一个 M × N 的矩阵时,我们需要找出岛屿个数,可以考虑如下思路:
从矩阵的第一行第一个元素开始遍历,直到遍历完整个矩阵。
当遍历到 '0' 时,直接跳过。
每当遍历到一个 '1' 时,说明找到了一个岛屿,将岛屿数量加 1,然后将这个 '1' 修改为 '0'。
此外,还需要使用 DFS 从该位置开始,向上下左右四个方向分别遍历,一样是,遇到 '0' 就跳过,遇到 '1' 就改成 '0',这时不需要在将岛屿数量加一了,因为我们现在处理的是第一次发现的那个子岛屿的相邻岛屿,他们整体只算一个岛屿,这样能够保证不会重复计数。
3. 完整代码
class Solution {
public:
void dfs(vector<vector<char>> &grid, int r, int c)
{
int nr = grid.size();
int nc = grid[0].size();
//如果越界或者遇到'0'就直接返回
if(r<0 || r>=nr || c<0 || c>=nc || grid[r][c] == '0')
{
return;
}
//能走到这说明遇到'1'了,把它改成'0'
grid[r][c] = '0';
//向上下左右四个方向继续找,找到了继续改
dfs(grid, r, c-1);
dfs(grid, r, c+1);
dfs(grid, r-1, c);
dfs(grid, r+1, c);
}
int solve(vector<vector<char> >& grid) {
int nr = grid.size();
if(nr == 0) return 0;
int nc = grid[0].size();
int island_num = 0;
for(int r=0; r<nr; r++)
{
for(int c=0;c<nc;c++)
{
if(grid[r][c] == '1')
{
//只需要在主循环中第一次遇见'1'时,将岛屿数量加一
//dfs会把,这个'1'上下左右连通的'1'全部改成'0',防止重复计数
island_num++;
dfs(grid, r, c);
}
}
}
return island_num;
}
};
4. 代码执行流程详解
为了能够更加深入地了解代码的执行过程,我以下面的矩阵为例,详细分析一下:
-
程序开始时,会先执行
solve函数。 -
solve函数中有一个嵌套的for循环,通过这个双层for循环我们可以遍历整个矩阵中的所有元素。 -
嵌套
for循环从第一行第一个元素,也就是(0,0)开始,发现这个位置是'1',于是island_num变成了 1,然后就会调用dfs(grid,0,0),此时solve函数被挂起,等待dfs执行结束。 -
进入
dfs(grid, 0, 0)之后,检查发现没有越界,并且位置(0,0)的值为'1',接着,将这个'1'改成'0'。再然后,向四个方向继续蔓延,处理掉所有连通的'1'。-
先来看
dfs(0, 0)的向右分支,也就是dfs(0, 1),发现值为'1',于是把它改成'0',然后继续蔓延。- 向左
(0,0),发现是'0',刚才改的。 - 向右
(0,2),这里本来就是'0'。 - 向上
(-1, 1),越界,直接返回。 - 向下
(1,1),本来就是'0',返回。 dfs(0, 1)执行完成,返回上一层。
- 向左
-
然后执行
dfs(0, 0)的向下分支,也就是dfs(1, 0),值为'1',改成'0'。- 它也会向四个方向蔓延,但是由于它周围,要不原本就是
'0',要不就是被我们处理成了'0',因此它向四个方向的dfs都会直接返回。 - 然后,
dfs(1, 0)执行完毕,返回上一层。
- 它也会向四个方向蔓延,但是由于它周围,要不原本就是
-
dfs(0, 0)的其他分支,向上和向左,都会越界,直接return,此时,左上角的连通块(0,0), (0,1), (1,0)已经全都变成了'0'。 -
然后,
dfs(0, 0)运行彻底结束,程序接着刚才挂起的solve函数继续执行。
-
-
接着进行
for循环,第一行第二个元素,这个位置原本是'1',但是已经被我们改成'0'了,于是直接跳过。 -
中间所有的元素已经全为
'0',全部跳过,直到第三行第三个元素。 -
发现是
'1',island_num变成 2,然后调用dfs(grid, 2, 2),重复上面的蔓延过程,最终这个位置也变成了'0'。
5. 代码优化
假设修改一下代码,对角线相连的 1 也算同一个岛屿,即 8 个方向都能扩散。请思考一下怎么改代码?
其实很简单,只用再加上四行对角线遍历的代码就行,让从当前位置可以向左上,左下,右上,右下蔓延。
void dfs(vector<vector<char>> &grid, int r, int c)
{
int nr = grid.size();
int nc = grid[0].size();
if(r<0 || r>=nr || c<0 || c>=nc || grid[r][c] == '0')
{
return;
}
grid[r][c] = '0';
dfs(grid, r, c-1);
dfs(grid, r, c+1);
dfs(grid, r-1, c);
dfs(grid, r+1, c);
//只需要添加下面四行即可
dfs(grid, r-1, c-1);
dfs(grid, r-1, c+1);
dfs(grid, r+1, c-1);
dfs(grid, r+1, c+1);
}
但是这段代码有 8 行 dfs 看起来有点难看,于是我们还可以进行如下修改:
//8个方向的偏移量,上下左右和4个对角线
int dr[] = {-1, 1, 0, 0, -1, -1, 1, 1};
int dc[] = {0, 0, -1, 1, -1, 1, -1, 1};
void dfs(vector<vector<char>>& grid, int r, int c)
{
//...越界检查...
grid[r][c] = '0';
//一个循环搞定所有方向
for (int i=0; i<8; i++)
{
dfs(grid, r + dr[i], c + dc[i]);
}
}
6. 岛屿问题变形
这道题目可能还会出一些变形,对于这些变形题只需要在上面代码中进行微调即可。
6.1 求最大岛屿面积
题目要求:不再是数有多少个岛,而是找出 面积最大,也就是包含 1 最多的那个岛。
代码微调:
dfs函数不再返回void,而是返回一个int,代表这次处理掉了多少个 1。- 让
dfs返回return 1 + dfs(上) + dfs(下) + dfs(左) + dfs(右)。 solve函数中添加max_area = max(max_area, dfs(r, c))。
6.2 求岛屿周长
题目要求:计算岛屿边缘的长度。
代码微调:
- dfs 终止条件:当你试图往海里走,也就是
grid == '0',或者越界时,说明你撞到了 边界。 - 每撞到一次边界,就
return 1。 - 最后把撞到的次数加起来
6.3 封闭岛屿
题目要求:如果一个岛屿靠着地图边缘,它就不算,只数那些被海水完全包围的。
代码微调:
- 写一个循环,把地图 四周边缘 上的 1 全都作为起点跑一遍 dfs,变成0。
- 将得到的矩阵,再用现在那套代码数剩下的岛屿。
本文完。