DFS/BFS 算法遍历二维数组-----岛屿问题解决

15 篇文章 0 订阅
13 篇文章 0 订阅

岛屿系列算法问题是经典的面试高频题,这里参考labuladong的一文秒杀所有岛屿问题,写出了关于C++的DFS/BFS的框架,供以后复习回顾。

框架

如果你把二维矩阵中的每一个位置看做一个节点,这个节点的上下左右四个位置就是相邻节点,那么整个矩阵就可以抽象成一幅网状的「图」结构。

DFS

// 二叉树遍历框架
void traverse(TreeNode* root) {
    traverse(root->left);
    traverse(root->right);
}
// 二维矩阵遍历框架
void dfs(vector<vector<int>> grid, int i, int j, vector<vector<bool>> visited) {
    int m = grid.size(), n = grid[0].size();
    if (i < 0 || j < 0 || i >= m || j >= n) {
        // 超出索引边界
        return;
    }
    if (visited[i][j]) {
        // 已遍历过 (i, j)
        return;
    }
    // 进入节点 (i, j)
    visited[i][j] = true;
    dfs(grid, i - 1, j, visited); // 上
    dfs(grid, i + 1, j, visited); // 下
    dfs(grid, i, j - 1, visited); // 左
    dfs(grid, i, j + 1, visited); // 右
}

BFS–有个关于所有BFS的易错点(对此类图结构来说)

注意,每个BFS,在对结果做处理的时候,都有两个方法:

  1. 当节点在队列取出来后,对节点做好记录处理
  2. 当节点放入队列的时候,就对节点做好记录处理

之前在图论的总结中,是统计个数(如拓扑排序等)的时候,第一种第二种都适用。但是像二分图中和本题中所需要做的记录处理涉及到取节点放入队列的时候,只能适用于第二种(因为有可能它还在队列里,你又一次把他放进去了。因此二叉树是没事,但是图结构会有事)。因此不要偷懒,对于图结构尽量使用第二种,多写点就多写点,但是不容易出错

// 二维矩阵遍历框架
 void bfs(vector<vector<char>>& grid,int i, int j)
    {
        deque<pair<int,int>> q;
        int rows = grid.size(),cols = grid[0].size();
        q.push_back({i,j});
        visited[i][j] = true;
        while(!q.empty())
        {
            tie(i,j) = q.front();
            q.pop_front();
            if(i < rows-1 && grid[i+1][j] == '1') 
            {
            	q.push_back({i+1,j});
            	visited[i+1][j] = true;
            }
            if(i>0 &&grid[i-1][j]=='1') 
            {
            	q.push_back({i-1,j});
            	visited[i-1][j] = true;
            }
            if(j<cols-1 &&grid[i][j+1]=='1') 
            {
            	q.push_back({i,j+1});
            	visited[i][j+1] = true;
            }
            if(j>0 &&grid[i][j-1]=='1') 
            {
            	q.push_back({i,j-1});
            	visited[i][j-1] = true;
            }
        }
    }

应用岛屿问题–常用DFS(代码简单些)

岛屿数量—边界为海

在这里插入图片描述
【题解】其实就是先发现一个岛屿,随后用dfs来遍历将其岛屿淹掉,随后岛屿数量加一,再去找有没有岛屿。为什么每次遇到岛屿,都要用 DFS 算法把岛屿「淹了」呢?主要是为了省事,避免维护 visited 数组。
下面为DFS应用

class Solution {
public:
	//主函数,执行找到岛屿,记录岛屿,淹掉岛屿
    int numIslands(vector<vector<char>>& grid) {
        int rows = grid.size(),cols = grid[0].size();
        int ans = 0;
        for(int i = 0; i<rows; ++i)
        {
            for(int j = 0; j<cols; ++j)
            {
                if(grid[i][j]=='1')
                {
                    ans++;
                    dfs(grid,i,j,rows,cols);
                }
            }
        }
        return ans;
    }
    //dfs--淹掉其他岛屿就行
    void dfs(vector<vector<char>>& grid,int i, int j, int rows, int cols)
    {
    	//两个退出条件,对应着超界&visited
        if(i<0||j<0||i>=rows||j>=cols) return;
        if(grid[i][j]=='0') return;
        //前序处理
        grid[i][j] = '0';
        //递归遍历
        dfs(grid,i+1,j,rows,cols);
        dfs(grid,i,j+1,rows,cols);
        dfs(grid,i-1,j,rows,cols);
        dfs(grid,i,j-1,rows,cols);
    }
};

下面为BFS应用

class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        int rows = grid.size(),cols = grid[0].size();
        int ans = 0;
        for(int i = 0; i<rows; ++i)
        {
            for(int j = 0; j<cols; ++j)
            {
                if(grid[i][j]=='1')
                {
                    ans++;
                    bfs(grid,i,j);
                }
            }
        }
        return ans;
    }
    void bfs(vector<vector<char>>& grid,int i, int j)
    {
        deque<pair<int,int>> q;
        int rows = grid.size(),cols = grid[0].size();
        q.push_back({i,j});
        grid[i][j] = '0';
        while(!q.empty())
        {
            tie(i,j) = q.front();
            q.pop_front();
            if(i < rows-1 && grid[i+1][j]=='1') {q.push_back({i+1,j});grid[i+1][j] = '0';}
            if(i>0 &&grid[i-1][j]=='1') {q.push_back({i-1,j});grid[i-1][j] = '0';}
            if(j<cols-1 &&grid[i][j+1]=='1') {q.push_back({i,j+1});grid[i][j+1] = '0';}
            if(j>0 &&grid[i][j-1]=='1') {q.push_back({i,j-1});grid[i][j-1] = '0';}
        }
    }
};

封闭岛屿的数量—边界为地

在这里插入图片描述
【题解】这样的话,相比上题,也就是岛的四条边需要先淹掉,剩下的中间再调用上面《岛屿数量》的解决方法即可!淹掉函数当然还是使用dfs即可。

class Solution {
public:
    int closedIsland(vector<vector<int>>& grid) {
        int rows = grid.size(),cols = grid[0].size();
        int ans = 0;
        for(int i=0;i<cols;i++)
        {
            dfs(grid,0,i);
            dfs(grid,rows-1,i);
        }
        for(int i =0 ;i<rows;i++)
        {
            dfs(grid,i,0);
            dfs(grid,i,cols-1);
        }
        for(int i = 1;i<rows;i++)
        {
            for(int j = 1;j<cols;j++)
            {
                if(grid[i][j]== 0) {ans++;dfs(grid,i,j);}
            }
        }
        return ans;
    }
    void dfs(vector<vector<int>>& grid,int i,int j)
    {
        int rows = grid.size(),cols = grid[0].size();
        if(i<0||j<0||i>=rows||j>=cols) return;
        if(grid[i][j] == 1) return;
        grid[i][j] = 1;
        dfs(grid,i+1,j);
        dfs(grid,i,j+1);
        dfs(grid,i-1,j);
        dfs(grid,i,j-1);
    }
};

被围绕的区域–保留与边界相连的点

在这里插入图片描述
其实,本题的实质和上面《与边界为地》是统计矩阵内部的岛屿,是同一个题。比较也是要处理边界和内陆。
因此,本题仍可以从先对边界处理,再对内陆处理的思想,不过,在对边界做处理时,要先做好标记,这样内陆处理完,再将边界返回回来即可。因此我们对边界处理先换成#号已视为做标记。

class Solution {
public:
    void solve(vector<vector<char>>& board) {
        int rows = board.size(),cols = board[0].size();
        for(int j = 0; j<cols; j++)
        {
            dfs1(board,0,j);
            dfs1(board,rows-1,j);
        }
        for(int i = 0; i<rows; i++)
        {
            dfs1(board,i,0);
            dfs1(board,i,cols-1);
        }
        for(int i = 1;i<rows-1;++i)
        {
            for(int j=1;j<cols-1;++j)
            {
                if(board[i][j] == 'O') dfs2(board,i,j);
            }
        }
        for(int i = 0;i<rows;++i)
        {
            for(int j=0;j<cols;++j)
            {
                if(board[i][j] == '#') board[i][j] = 'O';
            }
        }
        return ;
    }
    void dfs1(vector<vector<char>>& board,int i, int j)
    {
        int rows = board.size(),cols = board[0].size();
        if(i<0||j<0||i>=rows||j>=cols) return;
        if(board[i][j] == 'X'||board[i][j] == '#') return;
        board[i][j] = '#';
        dfs1(board,i+1,j);
        dfs1(board,i,j+1);
        dfs1(board,i-1,j);
        dfs1(board,i,j-1);
    }
    void dfs2(vector<vector<char>>& board,int i, int j)
    {
        int rows = board.size(),cols = board[0].size();
        if(i<0||j<0||i>=rows||j>=cols) return;
        if(board[i][j] == 'X') return;
        board[i][j] = 'X';
        dfs2(board,i+1,j);
        dfs2(board,i,j+1);
        dfs2(board,i-1,j);
        dfs2(board,i,j-1);
    }
};

岛屿的最大面积—计算面积

在这里插入图片描述

【题解】这题的大体思路和之前完全一样,只不过 我们设置的DFS函数淹没岛屿的同时,还需要去记录这个岛屿的面积。

class Solution {
public:
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int ans = 0;
        int rows = grid.size(),cols = grid[0].size();
        for(int i = 0;i<rows;i++)
        {
            for(int j = 0; j< cols; j++)
            {
                if(grid[i][j] == 1) ans = max(ans,dfs(grid,i,j));
            }
        }
        return ans;

    }
    //修改一下递归就可以让他出数字了!
    int dfs(vector<vector<int>>& grid,int x, int y)
    {
        int ans = 0;
        int rows = grid.size(),cols = grid[0].size();
        if(x<0||y<0||x>=rows||y>=cols) return 0;
        if(grid[x][y] == 0) return 0;
        grid[x][y] = 0;
        ans++;
        ans += dfs(grid,x+1,y);
        ans += dfs(grid,x-1,y);
        ans += dfs(grid,x,y+1);
        ans += dfs(grid,x,y-1);
        return ans;
    }
};

统计子岛屿—不是简单求交集!

在这里插入图片描述
在这里插入图片描述
【题解】正如题目, 这个题看实例2就得知,不是两边求了交随后统计就行的,其实是找完岛屿,然后只有严格子集关系才可以。因此我们可以采用两次遍历,第一次直接把不可能是子集关系的都淹掉(即但凡一个点为海,一个点为地,就应该把grid2那部分全部淹掉),第二次再对grid2进行统计就可以了。

class Solution {
public:
    int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
        int rows = grid2.size(),cols = grid2[0].size();
        int ans = 0;
        //第一次将不可能为子岛屿的全淹没掉
        for(int i = 0; i<rows; i++)
        {
            for(int j = 0;j<cols; j++)
            {
                if(grid2[i][j] ==1 && grid1[i][j] == 0) {dfs(grid2,i,j);}
            }
        }
        //第二次再统计
        for(int i = 0; i<rows; i++)
        {
            for(int j = 0;j<cols; j++)
            {
                if(grid2[i][j] ==1) {ans++;dfs(grid2,i,j);}
            }
        }
        return ans;
    }
    void dfs(vector<vector<int>>& grid2, int i, int j)
    {
        int rows = grid2.size(),cols = grid2[0].size();
        if(i<0||j<0||i>=rows||j>=cols) return;
        if(grid2[i][j]==0 ) return;
        grid2[i][j] = 0;
        dfs(grid2,i+1,j);
        dfs(grid2,i-1,j);
        dfs(grid2,i,j+1);
        dfs(grid2,i,j-1);
    }
};

不同岛屿的数量—统计不同形状

在这里插入图片描述
【题解】很显然我们得想办法把二维矩阵中的「岛屿」进行转化,变成比如字符串这样的类型,然后利用 HashSet 这样的数据结构去重,最终得到不同的岛屿的个数。(二叉树序列化)因此,需要根据以下原则来具体解决该题:
对于形状相同的岛屿,如果从同一起点出发,dfs 函数遍历的顺序肯定是一样的。例如:
在这里插入图片描述
实现时,因为要进去递归,因此撤销的4、5、6并不好表示,但是我们可以用-1、-2、-3.只要每次使用 dfs 遍历岛屿的时候生成这串数字进行比较,就可以计算到底有多少个不同的岛屿了。(图中因为在边缘,实际实现其实是有八个标号)
所以我们需要稍微改造 dfs 函数,添加一些函数参数以便记录遍历顺序:需要多个id统计动作以及string变量来记录顺序。

 void dfs(vector<vector<int>>& grid, int i, int j, string &record, int id)
    {
        int rows = grid2.size(),cols = grid2[0].size();
        if(i<0||j<0||i>=rows||j>=cols) return;
        if(grid[i][j]==0 ) return;
        grid[i][j] = 0;
        record.push_back(id+'0');
        dfs(grid2,i+1,j,record,1);
        dfs(grid2,i-1,j,record,2);
        dfs(grid2,i,j+1,record,3);
        dfs(grid2,i,j-1,record,4);
        record.push_back('-');
        record.push_back(id+'0');    }

所以整体的代码就是:

class Solution {
public:
    int numDistinctIslands(vector<vector<int>>& grid) {
    int rows = grid.size(), cols = grid[0].size();
    unordered_set<string> com;//用来统计有多少不同的岛屿
    for(int i=0;i<rows;i++)
    {
        for(int j=0;j<cols;j++)
        {
            if(grid[i][j] == 1)
            {
                string tem;
                dfs(grid,i,j,tem,0);
                com.insert(tem);
            }
        }
    }
    int n = com.size();
    return n;
    }
    void dfs(vector<vector<int>>& grid, int i, int j, string &record, int id)
    {
        int rows = grid.size(), cols = grid[0].size();
        if(i<0||j<0||i>=rows||j>=cols) return;
        if(grid[i][j] == 0) return;
        grid[i][j] = 0;
        record.push_back(id+'0');
        dfs(grid,i+1,j,record,1);
        dfs(grid,i-1,j,record,2);
        dfs(grid,i,j+1,record,3);
        dfs(grid,i,j-1,record,4);
        record.push_back('-');
        record.push_back(id+'0');
    }
};

最短的桥—DFS+BFS

在这里插入图片描述
这个题思路其实很好确定,主要是线区别开两个岛,然后通过扩散步数来找到另一个岛。
这里用DFS来区别分开两个岛,并且将边缘加入到队列中,随后用BFS来找到另一个岛。
【实现的细节】思路比较简单,但是实现起来确实复杂,容易出错。

  1. 可以不用设置visited来,只需要把第一次遇到的岛屿由1标志为2,即可区别两个岛,但关键是记得要退出两个for循环
  2. 在使用BFS时,勿忘在记录距离的同时记录哪些路是走过的
  3. 在使用BFS时,应注意到本文提出的易错点,要在加入时改变标记,防止重复多次放入!
class Solution {
public:
   int shortestBridge(vector<vector<int>>& grid) {
       int row = grid.size(), col = grid[0].size();
       deque<pair<int, int>>dq;
       for(int i = 0; i < row; ++i) {
           for(int j = 0; j < col; ++j) {
               if(grid[i][j] == 1) {
                   dfs(grid, i, j, dq);
                   break;
               }
           }
           if(!dq.empty()) break;
       }
       int ans = 0;
       while(!dq.empty()) {
           ++ans;
           int n = dq.size();
           for(int i = 0; i < n; ++i) {
               auto v = dq.front();
               dq.pop_front();
               if(v.first < row-1 && grid[v.first+1][v.second] == 0) {
                   grid[v.first+1][v.second] = 2;
                   dq.push_back({v.first+1, v.second});
               }
               else if(v.first < row-1 && grid[v.first+1][v.second] == 1) return ans;
               if(v.first > 0 && grid[v.first-1][v.second] == 0) {
                   grid[v.first-1][v.second] = 2;
                   dq.push_back({v.first-1, v.second});
               }
               else if(v.first > 0 && grid[v.first-1][v.second] == 1) return ans;
               if(v.second < col-1 && grid[v.first][v.second+1] == 0) {
                   grid[v.first][v.second+1] = 2;
                   dq.push_back({v.first, v.second+1});
               }
               else if(v.second < col-1 && grid[v.first][v.second+1] == 1) return ans;
               if(v.second > 0 && grid[v.first][v.second-1] == 0) {
                   grid[v.first][v.second-1] = 2;
                   dq.push_back({v.first, v.second-1});
               }
               else if(v.second > 0 && grid[v.first][v.second-1] == 1) return ans;
           }
       }
       return -1; 
   }
   void dfs(vector<vector<int>>& grid, int i, int j, deque<pair<int, int>>& dq) {
       int row = grid.size(), col = grid[0].size();
       if(i < 0 || j < 0 || i >= row || j >= col || grid[i][j] == 2) return;
       if(grid[i][j] == 0) {
           dq.push_back({i,j});
           grid[i][j] = 2;
           return;
       }
       grid[i][j] = 2;
       dfs(grid, i+1, j, dq);
       dfs(grid, i, j+1, dq);
       dfs(grid, i-1, j, dq);
       dfs(grid, i, j-1, dq);
   }
};

最大人工岛—允许你变一格,求最大面积

在这里插入图片描述
其实可以用递进的思想来思考:
首先,我们可以暴力地遍历——即把每个格从0变1,然后通过DFS去查看各个岛屿的面积从而找到面积最大的岛屿。但是这个缺点就是,每次都会遍历好多重复的岛屿。
因此,我们可以通过一个map提前记录好每个岛屿的面积。然后遍历每个格子从0变1,然后查看上下左右的格子都是属于哪个岛屿的,然后将这个几个岛屿的面积合并+1,最后尝试更新最大值即可。
【细节】
将岛屿合并的时候,注意要找个set来储存岛屿编号,毕竟一个岛屿的面积只能加一次!

class Solution {
public:
    unordered_map<int, int>area;
    int largestIsland(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector<vector<int>>tag(m, vector<int>(n,0));
        int ans = 0, id = 1;
        //先遍历一遍,将岛屿做好标记
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(grid[i][j] == 1) {
                    dfs(grid, tag, i, j, id++);
                    ans = max(ans, area[id-1]);//易错点,上面++了,此处勿忘记-1
                }
            }
        }
        //随后尝试每一个为0的格子置1 and 尝试更新最大面积
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(tag[i][j] == 0) {
                    unordered_set<int> st;
                    int S = 1;
                    if(i > 0 && tag[i-1][j] != 0) st.insert(tag[i-1][j]);
                    if(j > 0 && tag[i][j-1] != 0) st.insert(tag[i][j-1]);
                    if(i < m-1 && tag[i+1][j] != 0) st.insert(tag[i+1][j]);
                    if(j < n-1 && tag[i][j+1] != 0) st.insert(tag[i][j+1]);
                    for(auto &id : st) S += area[id];
                    ans = max(ans, S);
                }
            }
        }
        return ans;

    }
    void dfs(vector<vector<int>>& grid, vector<vector<int>>& tag, int i, int j, int id) {
        int m = grid.size(), n = grid[0].size();
        if(i < 0 || j < 0 || i>=m || j>=n || grid[i][j]==0) return;
        grid[i][j] = 0;
        tag[i][j] = id;
        area[id]++;
        dfs(grid, tag, i+1, j, id);
        dfs(grid, tag, i, j+1, id);
        dfs(grid, tag, i-1, j, id);
        dfs(grid, tag, i, j-1, id);
    }
};
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值