• 岛屿类问题通用解法与DFS框架


    参考链接:
    https://leetcode.cn/problems/number-of-islands/solution/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/

    看到一个岛屿类问题的通用解法文章,总结得比较到位通俗易懂。在此基础上,本人进行进一步总结与加工,同时用c++进行代码实现。

    1.网格类问题的 DFS 遍历方法

    我们所熟悉的 DFS(深度优先搜索)问题通常是在树或者图结构上进行的。而我们今天要讨论的 DFS 问题,是在一种「网格」结构中进行的。岛屿问题是这类网格 DFS 问题的典型代表。网格结构遍历起来要比二叉树复杂一些,如果没有掌握一定的方法,DFS 代码容易写得冗长繁杂。

    网格问题是由 m * n 个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。
    岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。我们把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿。
    在这样一个设定下,就出现了各种岛屿问题的变种,包括岛屿的数量、面积、周长等。不过这些问题,基本都可以用 DFS 遍历来解决。

    在这里插入图片描述

    2.DFS基本结构

    网格结构要比二叉树结构稍微复杂一些,它其实是一种简化版的图结构。要写好网格上的 DFS 遍历,我们首先要理解二叉树上的 DFS 遍历方法,再类比写出网格结构上的 DFS 遍历。我们写的二叉树 DFS 遍历一般是这样的:

    void traverse(TreeNode root) {
        // 判断 base case
        if (root == null) {
            return;
        }
        // 访问两个相邻结点:左子结点、右子结点
        traverse(root.left);
        traverse(root.right);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到,二叉树的 DFS 有两个要素:「访问相邻结点」和「判断 base case」。

    第一个要素是访问相邻结点。二叉树的相邻结点非常简单,只有左子结点和右子结点两个。二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。那么我们的 DFS 遍历只需要递归调用左子树和右子树即可。

    第二个要素是 判断 base case。一般来说,二叉树遍历的 base case 是 root == null。这样一个条件判断其实有两个含义:一方面,这表示 root 指向的子树为空,不需要再往下遍历了。另一方面,在 root == null 的时候及时返回,可以让后面的 root.left 和 root.right 操作不会出现空指针异常。

    对于网格上的 DFS,我们完全可以参考二叉树的 DFS,写出网格 DFS 的两个要素:

    首先,网格结构中的格子有多少相邻结点?答案是上下左右四个。对于格子 (r, c) 来说(r 和 c 分别代表行坐标和列坐标),四个相邻的格子分别是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1)。换句话说,网格结构是「四叉」的。

    在这里插入图片描述
    其次,网格 DFS 中的 base case 是什么?从二叉树的 base case 对应过来,应该是网格中不需要继续遍历、grid[r][c] 会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。

    在这里插入图片描述
    这一点稍微有些反直觉,坐标竟然可以临时超出网格的范围?这种方法我称为「先污染后治理」—— 甭管当前是在哪个格子,先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null 再返回。

    因此,网格DFS遍历的框架代码为

    void dfs(int[][] grid, int r, int c) {
        // 判断 base case
        // 如果坐标 (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);
    }
    
    // 判断坐标 (r, c) 是否在网格中
    boolean inArea(int[][] grid, int r, int c) {
        return 0 <= r && r < grid.length 
            	&& 0 <= c && c < grid[0].length;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.如何避免重复遍历

    网格结构的 DFS 与二叉树的 DFS 最大的不同之处在于,遍历中可能遇到遍历过的结点。这是因为,网格结构本质上是一个「图」,我们可以把每个格子看成图中的结点,每个结点有向上下左右的四条边。在图中遍历时,自然可能遇到重复遍历结点。

    这时候,DFS 可能会不停地「兜圈子」,永远停不下来。

    如何避免这样的重复遍历呢?答案是标记已经遍历过的格子。以岛屿问题为例,我们需要在所有值为 1 的陆地格子上做 DFS 遍历。每走过一个陆地格子,就把格子的值改为 2,这样当我们遇到 2 的时候,就知道这是遍历过的格子了。也就是说,每个格子可能取三个值:

    0 —— 海洋格子
    1 —— 陆地格子(未遍历过)
    2 —— 陆地格子(已遍历过)

    我们在框架代码中加入避免重复遍历的语句:

    void dfs(int[][] grid, int r, int c) {
        // 判断 base case
        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);
    }
    
    // 判断坐标 (r, c) 是否在网格中
    boolean inArea(int[][] grid, int r, int c) {
        return 0 <= r && r < grid.length 
            	&& 0 <= c && c < grid[0].length;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这样,我们就得到了一个岛屿问题、乃至各种网格问题的通用 DFS 遍历方法。以下所讲的几个例题,其实都只需要在 DFS 遍历框架上稍加修改而已。

    4.leetcode 200 Number of Islands

    给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
    岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
    此外,你可以假设该网格的四条边均被水包围。

    输入:grid = [
    [“1”,“1”,“1”,“1”,“0”],
    [“1”,“1”,“0”,“1”,“0”],
    [“1”,“1”,“0”,“0”,“0”],
    [“0”,“0”,“0”,“0”,“0”]
    ]
    输出:1

    输入:grid = [
    [“1”,“1”,“0”,“0”,“0”],
    [“1”,“1”,“0”,“0”,“0”],
    [“0”,“0”,“1”,“0”,“0”],
    [“0”,“0”,“0”,“1”,“1”]
    ]
    输出:3

    如果套用上面的框架,我们可以比较容易想到下面的解法
    扫描整个二维网格,如果某个位置为1,则以其为起点进行DFS。在进行DFS过程中,每次搜索到1,都会被标记成为0,避免重复搜素。

    #include<iostream>
    #include<vector>
    using namespace std;
    
    bool inArea(vector<vector<string>> &grid, int r, int c) {
        return r>=0 && r<grid.size() && c>=0 && c<grid[0].size();
    }
    
    void dfs(vector<vector<string>> &grid, int r, int c) {
        if (!inArea(grid, r, c))  return;
        if (grid[r][c]=="0") return;
        grid[r][c] = "0";
        dfs(grid, r-1, c);
        dfs(grid, r+1, c);
        dfs(grid, r, c-1);
        dfs(grid, r, c+1);
    }
    
    void run() {
        //vector<vector<string>> grid = {{"1","1","1","1","0"}, {"1","1","0","1","0"}, 
        //                                {"1","1","0","0","0"}, {"0","0","0","0","0"}};
        vector<vector<string>> grid = {{"1","1","0","0","0"}, {"1","1","0","0","0"},
                                        {"0","0","1","0","0"}, {"0","0","0","1","1"}};
        int row = grid.size(), col = grid[0].size();
        int ansnum = 0;
        for(int i=0; i<row; i++) {
            for(int j=0; j<col; j++) {
                if (grid[i][j]=="1") {
                    ansnum++;
                    dfs(grid, i, j);
                }
            }
        }
        cout<<"ansnum is: "<<ansnum<<endl;
    }
    
    int main(int argc, char const *argv[])
    {
        run();
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    上面的代码中,inArea就是判断bad case。如果不在area区域内,直接返回。

    if (grid[r][c]==“0”),说明不是岛屿,直接返回。
    grid[r][c] = “0”, 则是将岛屿标记为已遍历,避免后面重复遍历。

    4. leetcode 695 Max Area of Island

    给你一个大小为 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 。

    #include<iostream>
    #include<vector>
    using namespace std;
    
    bool inArea(vector<vector<int>> &grid, int r, int c) {
        return r>=0 && r<grid.size() && c>=0 && c<grid[0].size();
    }
    
    int dfs(vector<vector<int>> &grid, int r, int c) {
        if (!inArea(grid, r, c)) {
            return 0;
        }
        if (grid[r][c] != 1) {
            return 0;
        }
        grid[r][c] = 0;
        return 1 + dfs(grid, r-1, c)+dfs(grid, r+1, c)+dfs(grid, r, c-1)+dfs(grid, r, c+1);
    }
    
    void run() {
        vector<vector<int>> 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}};
    
        int maxarea = 0;
        int row=grid.size(), col=grid[0].size();
        for(int i=0; i<row; i++) {
            for(int j=0; j<col; j++) {
                if (grid[i][j]==1) {
                    int curarea = dfs(grid,i,j);
                    maxarea = max(curarea, maxarea);
                }
            }
        }
        std::cout<<"maxarea is: "<<maxarea<<endl;
    }
    
    int main(int argc, char const *argv[])
    {
        run();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    与前面求岛屿个数的唯一在于,dfs过程中求面积时,如果遇到1,则当前岛屿的面积在四个方向dfs并将面积加1。

    5.LeetCode 463. Island Perimeter

    给定一个 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 个黄色的边

    #include<iostream>
    #include<vector>
    using namespace std;
    
    bool isArea(vector<vector<int>> grid, int r, int c) {
        return r>=0 && r<grid.size() && c>=0 && c<grid[0].size();
    }
    
    int dfs(vector<vector<int>> grid, int r, int c) {
        // 出了岛的边界范围,周长加1
        if (!isArea(grid, r, c)) {
            return 1;
        }
        // 此时碰到的是海水,周长加1
        if (grid[r][c] == 0) {
            return 1;
        }
        if (grid[r][c] != 1) {
            return 0;
        }
        // 注意此时不能赋值为0
        grid[r][c] = -1;
        return dfs(grid, r-1, c)+dfs(grid, r+1, c)+dfs(grid, r, c-1)+dfs(grid, r, c+1);
    }
    
    void run() {
        vector<vector<int>> grid = {{0,1,0,0}, {1,1,1,0}, {0,1,0,0}, {1,1,0,0}};
        int result = 0;
        for(int i=0; i<grid.size(); i++) {
            for(int j=0; j<grid[0].size(); j++) {
                if (grid[i][j]==1) {
                    result = dfs(grid, i, j);
                }
            }
        }
        cout<<"result is: "<<result<<endl;
    }
    
    int main(int argc, char const *argv[])
    {
        run();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    大致的思路也是与上面相同,唯一的不同点,在于周长的计算逻辑,具体可以参考代码中注释。

  • 相关阅读:
    小红书《乡村振兴战略下传统村落文化旅游设计》中南大博士许少辉八一新著
    android10.0(Q) MTK 6765 user版本打开root权限
    【计算思维】少儿编程蓝桥杯青少组计算思维题考试真题及解析B
    Spring Boot中使用Redis进行大数据缓存
    Kafka简单汇总
    Go语言学习笔记—xorm
    学会使用Pandas进行数据清洗
    想参加专转本,线上课怎样呀?
    npm ERR! code ERESOLVEnpm ERR! ERESOLVE could not resolve dependency
    利用PyTorch训练模型识别数字+英文图片验证码
  • 原文地址:https://blog.csdn.net/bitcarmanlee/article/details/125479801