前言
洪水填充是一种用在图上的搜索算法,其过程就像洪水或病毒一样逐渐蔓延整个区域,继而达到遍历和统计相同属性的连通区域的功能,中间也可以通过每走过一个节点就设置路径信息的方法来达到剪枝的效果。
一、岛屿数量——洪水填充方法
class Solution {
public:int numIslands(vector<vector<char>>&grid){return solve2(grid);}//洪水填充方法int solve2(vector<vector<char>>&grid){int n=grid.size();int m=grid[0].size();int ans=0;for(int i=0;i<n;i++){for(int j=0;j<m;j++){if(grid[i][j]=='1'){dfs(i,j,grid);ans++;}}}return ans;}void dfs(int i,int j,vector<vector<char>>&grid){if(i<0||i==grid.size()||j<0||j==grid[0].size()||grid[i][j]!='1'){return ;}grid[i][j]=0;dfs(i,j+1,grid);dfs(i,j-1,grid);dfs(i+1,j,grid);dfs(i-1,j,grid);}//并查集方法vector<int>father;int sets;int solve1(vector<vector<char>>& grid) {int n=grid.size();int m=grid[0].size();build(n,m,grid);for(int i=0;i<n;i++){for(int j=0;j<m;j++){if(grid[i][j]=='1'){if(j>0&&grid[i][j-1]=='1')//左{Union(index(i,j,m),index(i,j-1,m));}if(i>0&&grid[i-1][j]=='1')//上{Union(index(i,j,m),index(i-1,j,m));}}}}return sets;}void build(int n,int m,vector<vector<char>>&grid){father.resize(n*m);sets=0;int idx;//只有为“1”的格子才计算for(int i=0;i<n;i++){for(int j=0;j<m;j++){if(grid[i][j]=='1'){idx=index(i,j,m);father[idx]=idx;sets++;}}}}int find(int i){if(i!=father[i]){father[i]=find(father[i]);}return father[i];}void Union(int x,int y){int fx=find(x);int fy=find(y);if(fx!=fy){father[fx]=fy;sets--;}}int index(int i,int j,int m){return i*m+j;}
};
这个题如果看过我上一篇文章数据结构与算法:并查集的话对这个题应该不陌生,除了可以用并查集,还可以用洪水填充做。
根据洪水填充的过程,观察题目就可以发现,当出现一个“1”时,就开始洪水填充,感染所有连通的区域,相当于遍历完了一整个岛屿,所以要把所有点都改成0,然后让ans++即可。
这里洪水填充的函数采用了dfs(深度优先搜索)方法,当然也可以改成广度优先。如果没有越界或者碰到“水”,就先修改当前点为0,然后去上下左右感染。
二、被围绕的区域
class Solution {
public:void solve(vector<vector<char>>& board) {int n=board.size();int m=board[0].size();//先感染边界‘O’,画出不能修改的for(int j=0;j<m;j++){if(board[0][j]=='O'){dfs(0,j,board);}if(board[n-1][j]=='O'){dfs(n-1,j,board);}}for(int i=0;i<n;i++){if(board[i][0]=='O'){dfs(i,0,board);}if(board[i][m-1]=='O'){dfs(i,m-1,board);}}//修改被包围的‘O’for(int i=0;i<n;i++){for(int j=0;j<m;j++){if(board[i][j]=='O'){board[i][j]='X';}if(board[i][j]=='F'){board[i][j]='O';}}}}void dfs(int i,int j,vector<vector<char>>&board){if(i<0||i==board.size()||j<0||j==board[0].size()||board[i][j]!='O'){return ;}board[i][j]='F';dfs(i-1,j,board);dfs(i+1,j,board);dfs(i,j-1,board);dfs(i,j+1,board);}
};
这个题就需要一点预处理了,因为边界处是默认无法被包围的,所以上来先顺着边界摸一遍,感染在边界位置无法被包围的“O”。那么之后出现的“O”就都是可以被包围的了,就遍历整个格子,有“O”就改成“X”,然后因为之前把边界处“O”改成了“F”,所以需要改回来。
三、最大人工岛
class Solution {
public:int largestIsland(vector<vector<int>>& grid) {int n=grid.size();//设置编号int id=2;for(int i=0;i<n;i++){for(int j=0;j<n;j++){if(grid[i][j]==1){dfs(i,j,grid,id++);}}}//统计大小map<int,int>size;int ans=0;for(int i=0;i<n;i++){for(int j=0;j<n;j++){if(grid[i][j]>1){ans=max(ans,++size[grid[i][j]]);}}}//遍历‘0’vector<bool>visited(id,false);//去重int up,down,left,right,sum;for(int i=0;i<n;i++){for(int j=0;j<n;j++){if(grid[i][j]==0){ up=i==0?0:grid[i-1][j];down=i==n-1?0:grid[i+1][j];left=j==0?0:grid[i][j-1];right=j==n-1?0:grid[i][j+1];visited[up]=true;sum=size[up]+1;if(!visited[down]){sum+=size[down];visited[down]=true;}if(!visited[left]){sum+=size[left];visited[left]=true;}if(!visited[right]){sum+=size[right];visited[right]=true;}ans=max(ans,sum);visited[up]=false;visited[down]=false;visited[left]=false;visited[right]=false;}}}return ans;}void dfs(int i,int j,vector<vector<int>>&grid,int id){if(i<0||i==grid.size()||j<0||j==grid[0].size()||grid[i][j]!=1){return ;}grid[i][j]=id;dfs(i-1,j,grid,id);dfs(i+1,j,grid,id);dfs(i,j-1,grid,id);dfs(i,j+1,grid,id);}
};
这个题就需要点思考了,首先分析题目可以想出整体思路就是先统计已经是岛屿的区域,接着再遍历每一个“0”,去看周围是否有岛屿。
接着就是分步解决问题,首先是统计已经是岛屿的区域,这个就是碰到是“1”的点洪水填充即可。但由于之后再遍历“0”的时候要获取周围岛屿的大小,所以要知道每一个岛屿自己的大小,所以考虑在洪水填充时给每个岛屿打上编号,接着设置一个map存岛屿编号对应大小。需要注意的是,由于最后让返回最大岛屿的大小,当情况是原装岛就是最大的时,要返回原装岛的大小,所以在这一步就要统计ans最大值。
之后是遍历“0”的过程,由于当一个“0”周围存在同一片岛屿时,这个岛屿的大小加一次就够了,所以还要考虑去重。具体方法就是设置一个visited数组,来到就设为true。之后遍历,每次统计周围岛屿的大小之和即可。
四、打砖块
class Solution {
public:vector<int> hitBricks(vector<vector<int>>& grid, vector<vector<int>>& hits) {//思想————“时光倒流”->炮弹倒着看->之前炮弹对当前有影响int n=grid.size();int m=grid[0].size();int h=hits.size();vector<int>ans(h,0);//特例if(n==1){return ans;}//先让命中位置-1for(int i=0;i<h;i++){grid[hits[i][0]][hits[i][1]]--;}//天花板for(int j=0;j<m;j++){if(grid[0][j]==1){dfs(0,j,grid);}}//“时光倒流”for(int i=h-1;i>=0;i--){grid[hits[i][0]][hits[i][1]]++;//命中位置补回来if(worth(hits[i][0],hits[i][1],n,m,grid)){ans[i]=dfs(hits[i][0],hits[i][1],grid)-1;}}return ans;}//返回新增的安全格子数int dfs(int i,int j,vector<vector<int>>&grid){if(i<0||i==grid.size()||j<0||j==grid[0].size()||grid[i][j]!=1){return 0;}grid[i][j]=2;return 1+dfs(i-1,j,grid)+dfs(i+1,j,grid)+dfs(i,j-1,grid)+dfs(i,j+1,grid);}//是否值得填充bool worth(int i,int j,int n,int m,vector<vector<int>>&grid){return grid[i][j]==1&& //有效命中(i==0|| //在天花板上(i>0&&grid[i-1][j]==2)||(i<n-1&&grid[i+1][j]==2)||(j>0&&grid[i][j-1]==2)||(j<m-1&&grid[i][j+1]==2)); //周围有“2”}
};
这个题的思路就非常巧妙了,属于是初见肯定想不到的那种。
分析题目,由于之前的炮弹对当前的炮弹有影响,换言之,能让下面全部掉落的炮弹一定是最后一发,前面与之相关的炮弹可以理解为都是辅助炮,那么这里可以用到一个思想——“时光倒流”。“时光倒流”的方法就是,先只遍历一遍炮弹的命中位置,接着从后往前看炮弹,统计掉落的砖块。
所以首先只遍历炮弹命中位置,让该位置-1即可。
之后,考虑天花板处连接的砖块,直接连接天花板的砖块洪水填充即可。
经历上述过程后,肯定有剩余的“1”,这些就是会被炮弹打下来的砖块,所以此时进行“时光倒流”。首先把命中位置补回来,接下来的worth函数表示是否值得去洪水填充,即满足打中的是砖块且要么在天花板,可以连接下面的砖块,要么周围有“2”,即和天花板连接的砖块,那么就可以去洪水填充。因为要统计数量,所以这里的dfs函数需要返回连通砖块的个数,所以只需要返回四周之和再加自己的1即可。注意题目描述被命中的砖块不算,所以ans要再减去1。
总结
其实感觉洪水填充和并查集有异曲同工之处,就像这几个题感觉都能改并查集,只是洪水填充会更快。而且洪水填充只在情况比较简单的题目里速度比并查集快,当面对上一篇里最后感染源头那种题时的发挥空间就比较有限了,所以这里就更体现出并查集给集合打标签的这个用法的优势了。
还剩不到一个月,要加快速度了啊!期待接下来的图论和动态规划章节!!