谈到DFS问题,通常在树或者图结构中进行,今天讨论的岛屿问题专题实则是对网络中的 DFS 进行讨论,是在 【网格】中进行的。
网格问题的 DFS 遍历写法:
网络问题的基本概念:
我们首先针对网格问题中的网格结构定义,网格问题是由 m * n 个小方格构成的一个网格,每个小方格相邻的网格分别是上、下、左、右,所以在谈论网格的 DFS 深度优先遍历时我们需要对网格中上下左右四个方向进行遍历。
岛屿问题中的 0 一般表示为水,而 1 一般表示为陆地,而一个岛屿通常是指连续为 1(陆地)的小方格构造成不规则块。以此为基础就引发了多种问题的拓展。
DFS 的基本结构
网格结构要比二叉树略微复杂,我们先来看一段二叉树 DFS 遍历模板代码:
void traverse(TreeNode* root){
// 判断 base case
if(root) return;
// 访问两个相邻结点:左子结点和右子结点
traverse(root->left);
tarverse(root->right);
}
可以看到二叉树的遍历围绕两个因素展开:1.访问相邻的结点 2.判断base case
第一个要素是访问相邻节点,在二叉树中的相邻节点就是左子节点和右子结点。而对于我们的网格结构其实就是访问上、下、左、右四个相邻结点
第二个要素就是判断 base case。对于二叉树来说判断 base case 就是判断相邻子结点是否是 nullptr,而对于我们的网格结构就是判断相邻结点是否满足题意:首先相邻节点一定不能越界,其次相邻的结点一定要符合题目的意思。
void dfs(vector<vector<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)是否在网格中
bool inArea(vector<vector<int>>& grid,int r,int c){
return r>=0 && r<grid.size() && c>=0 && c<grid[0].size();
}
如何避免重复遍历
网格结构的 DFS 与二叉树的 DFS 最大的不同之处在于,遍历中可能遇到遍历过的结点。这是因为,网格结构本质上是一个图,我们可以把每个格子看作是图中的结点,两个结点有上下左右四条边。在图中遍历时,自然可能遇到重复的遍历结点。
这时候,DFS 可能会不停地来回遍历最终导致超时,如果图所示:
所以如何去避免这种情况呢?类似于一种记忆化搜索,我们需要去记录我们所遍历过的结点,在下一次访问到这些已经遍历过的结点时,会先进行判断如果已经遍历过了,那么return返回。
我们可以采用如下方法进行记录:
0----水 1----陆地(等待遍历) 2----陆地(已经遍历)
于是改进后的代码如下:
void dfs(vector<vector<int>>& grid,int r,int c){
// 判断base case
// 如果坐标(r,c)超出了网格范围直接返回
if(!inArea(grid,r,c)) return;
// 如果格子不是陆地直接返回
if(!grid[r][c]) 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)是否在网格中
bool inArea(vector<vector<int>>& grid,int r,int c){
return r>=0 && r<grid.size() && c>=0 && c<grid[0].size();
}
这样岛屿问题的 DFS 通用模板就出来了:同时对于大多数问题我们可以做如下优化:
在一些题解中,可能会把「已遍历过的陆地格子」标记为和海洋格子一样的 0,美其名曰「陆地沉没方法」,即遍历完一个陆地格子就让陆地「沉没」为海洋。这种方法看似很巧妙,但实际上有很大隐患,因为这样我们就无法区分「海洋格子」和「已遍历过的陆地格子」了。如果题目更复杂一点,这很容易出 bug。
有关岛屿问题的一些问题(Easy --> Hard)
针对 LeetCode 中出现的岛屿专题问题我在进行了一轮练习诸如此类的岛屿问题做一个个人总结记录:
首先第一题肯定是简单题打头阵:
463. 岛屿的周长 - 力扣(Leetcode)
给定一个
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 个黄色的边
示例 2:
输入:grid = [[1]] 输出:4
示例 3:
输入:grid = [[1,0]] 输出:4
在观察完上面的题目之后,我们可以很容易的发现此题的思路其实较为简单,毕竟是简单题不会很复杂,我们只需要整理整个grid对于每一个 grid [i] [j] 如果是1表示陆地此时我们记录该位置的贡献值(即上下左右是否满足贡献条件:越界的情况下贡献值+1,附近为 0(水)的情况下贡献值+1),如果 grid [i] [j] = 0 表示水,无需记录贡献值。所有很容易写出以下代码:
class Solution {
public:
// 方向数组
int dx[4] = {0, 1, 0, -1};
int dy[4] = {1, 0, -1, 0};
int islandPerimeter(vector<vector<int>> &grid) {
int n=grid.size(),m=grid[0].size();
int res=0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (grid[i][j]) {
int cnt=0;
for(int k=0;k<4;++k){
int x=i+dx[k];
int y=j+dy[k];
if(x<0 || x>=n || y<0 || y>=m || !grid[x][y]) ++cnt;
}
res+=cnt;
}
}
}
return res;
}
};
200. 岛屿数量 - 力扣(Leetcode)
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1
示例 2:
输入:grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] 输出:3
对于岛屿数量问题,我们可以做如下思考:
一个岛屿即一块不规则值为1且相邻的格子的集合,我们需要寻找岛屿数量(用res记录岛屿的数量):
1. 首先对所有格子进行遍历,当遍历到一个值为1的格子时,岛屿的数量res+=1)说明此时一定存在一块岛屿
2. 然后对这个格子进行深度优先遍历,对于其相邻的为陆地的格子进行记录:
即将其相邻的格子 grid[i][j] =1 改为 grid[i][j] = 0 。类似于病毒感染的原理。
代码如下:
class Solution {
private:
void dfs(vector<vector<char>>& grid,int r,int c){
int nr=grid.size();
int nc=grid[0].size();
grid[r][c]='0';
if(r-1>=0 && grid[r-1][c]=='1') dfs(grid,r-1,c);
if(r+1<nr && grid[r+1][c]=='1') dfs(grid,r+1,c);
if(c-1>=0 && grid[r][c-1]=='1') dfs(grid,r,c-1);
if(c+1<nc && grid[r][c+1]=='1') dfs(grid,r,c+1);
}
public:
int numIslands(vector<vector<char>>& grid){
int nrow=grid.size();
if(!nrow) return 0;
int ncolumn=grid[0].size();
int res=0;
for(int row=0;row<nrow;++row){
for(int column=0;column<ncolumn;++column){
if(grid[row][column]=='1'){
++res;
dfs(grid,row,column);
}
}
}
return res;
}
};
给你一个大小为 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 。
示例 2:
输入:grid = [[0,0,0,0,0,0,0,0]] 输出:0
此题只需要对每个岛屿进行 DFS 遍历求得每个岛屿的面寻找最大面积即可
具体代码如下
class Solution {
public:
int res=0;
int n=0;
int m=0;
int dx[4]={-1, 0, 1, 0};
int dy[4]={0, -1, 0, 1};
int dfs(vector<vector<int>>& grid,int row,int column){
if(row<0 || column<0 || row>=n || column>=m ||!grid[row][column]) return 0;
int ans=1;
grid[row][column]=0;
for(int i=0;i<4;++i){
ans += dfs(grid,row+dx[i],column+dy[i]);
}
return ans;
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
n=grid.size();
m=grid[0].size();
int res=0;
for(int i=0;i<n;++i){
for(int j=0;j<m;++j){
if(grid[i][j]){
res=max(res,dfs(grid,i,j));
}
}
}
return res;
}
};
给你一个大小为 n x n
二进制矩阵 grid
。最多 只能将一格 0
变成 1
。
返回执行此操作后,grid
中最大的岛屿面积是多少?
岛屿 由一组上、下、左、右四个方向相连的 1
形成。
示例 1:
输入: grid = [[1, 0], [0, 1]] 输出: 3 解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。
示例 2:
输入: grid = [[1, 1], [1, 0]] 输出: 4 解释: 将一格0变成1,岛屿的面积扩大为 4。
示例 3:
输入: grid = [[1, 1], [1, 1]] 输出: 4 解释: 没有0可以让我们变成1,面积依然为 4
在完成此题之前我们需要了解一种数据结构:并查集
【算法与数据结构】—— 并查集_酱懵静的博客-CSDN博客_并查集
在初步了解并查集之后我们不妨再来思考此题的思路:
在最多将一个0变为1的情况下,我们很容易想到此题考得其实就是岛屿合并问题,既然需要用到合并,并查集又恰好是为合并引入的一个高级数据结构,那么我们不妨试试并查集来解决此题。
并查集的创建查询以及合并相信不用多做赘述,并查集中的rank即是代表以 parent[x] 为代表的这一块岛屿的面积,所以首先我们需要进行一次 grid 遍历:
在这一次遍历中,我们会对并查集进行合并操作,合并所有的格子形成岛屿,并计算每一个岛屿的面积。在第一次遍历中我们仅仅在不改变任何值的情况进行。
之后根据题意我们可以选择最多一个格子将 0 改为 1 :
由此我们可以进行第二次遍历,而第二次遍历主要是为了寻找每个值为 0 的格子,通过将其变为1致使两个不相邻的岛屿进行合并,从而更新岛屿的最大面积。
总结上面的合并步骤大致如下:
1. 第一次遍历:寻找值为 1 的网格,将每个值为 1 的网格(陆地)合并为岛屿
2. 第二次遍历:寻找值为 0 的网格,将每个已经的岛屿通过值为 0 的网格进行合并
完整代码如下:
// 熟练掌握并查集类的模板
class UnionFind{
public:
int count;
vector<int> parent;
vector<int> rank;
// 构造方法
UnionFind(int n){
count=n;
parent=vector<int>(n);
rank=vector<int>(n,1);
// 根节点的初始化
for(int i=0;i<parent.size();++i){
parent[i]=i;
}
}
// 查找根节点
int Find(int x){
if(parent[x]!=x) parent[x]=Find(parent[x]);
return parent[x];
}
// 并查集的合并
void Union(int x,int y){
// 首先分别查找x 和 y的父节点
int root_x=Find(x),root_y=Find(y);
// 如果父节点相同则返回
if(root_x==root_y) return;
// 如果父节点不相同则比较两个 rank[root] 的值
// 小的父节点合并到大的父节点上完成合并
if(rank[root_x]>=rank[root_y]){
parent[root_y]=root_x;
rank[root_x]+=rank[root_y];
}
else{
rank[root_y]+=rank[root_x];
parent[root_x]=root_y;
}
}
};
class Solution{
public:
int largestIsland(vector<vector<int>>& grid){
// 遍历整个grid并创建并查集
int n=grid.size();
UnionFind uf(n*n);
// 声明
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
for(int i=0;i<n;++i){
for(int j=0;j<n;++j){
if(grid[i][j]){
for(int k=0;k<4;++k){
int nx=i+dx[k];
int ny=j+dy[k];
if(nx>=0 && nx<n && ny>=0 && ny<n && grid[nx][ny]){
// 将两个位置的值进行并查集的合并
uf.Union(i*n+j,nx*n+ny);
}
}
}
}
}
int maxArea=0;
// 遍历所有的0,判断如果这个0变为1之后最大的面积是多少
for(int i=0;i<n;++i){
for(int j=0;j<n;++j){
if(grid[i][j]) maxArea=max(maxArea,uf.rank[uf.Find(i*n+j)]);
else if(grid[i][j]==0){
int area=1;
set<int> seen;
// 在值为 0 的节点四周寻找是否存在1的点
for(int k=0;k<4;++k){
int nx=i+dx[k];
int ny=j+dy[k];
// 如果存在值为 1 的点 即水周围存在陆地 则利用并查集进行两地面积求和
if(nx>=0 && nx<n && ny>=0 && ny<n && grid[nx][ny]){
// 找到下一个遍历节点 的父节点
int root_x=uf.Find(nx*n+ny);
// 如果该节点的 父节点未被访问
if(seen.count(root_x)==0){
area += uf.rank[root_x];
seen.insert(root_x);
}
}
}
maxArea=max(maxArea,area);
}
}
}
return maxArea;
}
};