DFS连通域统计:岛屿数量问题及其变形

0 阅读6分钟

0.前言

本文我们来学习一下算法题中颇为著名的岛屿数量问题,我将会从问题本身入手,详细分析解题思路,给出完整代码并进行解析,最后简单了解一下几个岛屿问题的变种题目。

1. 问题描述

题目给出一个只含有 0 和 1 矩阵,1 代表是陆地,0 代表海洋, 如果两个 1 相邻,那么这两个 1 属于 同一个岛,我们暂时只考虑 上下左右为相邻

上下左右相邻的陆地可以组成一个岛屿,我们的目标是计算 岛屿的个数

当输入为下图的矩阵时,共有三个岛屿,因此输出应该为 3 。

1. daoyv.png

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. 代码执行流程详解

为了能够更加深入地了解代码的执行过程,我以下面的矩阵为例,详细分析一下:

2. 图二.png

  • 程序开始时,会先执行 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。
  • 将得到的矩阵,再用现在那套代码数剩下的岛屿。

本文完。