DFS
深度优先搜索算法(Depth First Search,简称DFS):一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止,属于盲目搜索。
基本框架
void DFS(形参列表){
if(判断边界){
相应操作;
}
尝试每一种可能{
满足深搜条件
标记
继续向深度方向前进DFS(传参)
恢复初始状态(若是回溯,可能会用到这一步)
}
}
在我看来,DFS的主要思想如下:
1.不撞南墙不回头:深度前进
2.撞了南墙回头找其他方向的墙:折返尝试其他路线
3.撞完回家找妈妈哭诉自己撞了墙:返回结果
岛屿的周长
给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。
网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100。计算这个岛屿的周长。
分析
对于这道题,其实迭代就可以做。
一个岛屿的周长计算方法无非就是所有是边界的边和紧邻水域的边的长度总和。
那么我们只需要找到grid[i][j]=1
的格子,看它的四个方向是否为边界或者紧邻水域即可,是则周长加1,否则看其他边是否满足条件。
使用DFS也是一样的思路。
code
//迭代法
class Solution {
public:
int dir[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};//右左上下四个方向
int islandPerimeter(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
int res = 0;
for(int i = 0;i<m;++i){
for(int j = 0;j<n;++j){
if(grid[i][j]){//是陆地
int cnt = 0;//这块陆地的周长计数器
for(int k = 0;k<4;++k){//四个方向查探
int nx = i + dir[k][0];
int ny = j + dir[k][1];
if(nx<0||ny<0||nx>=m||ny>=n||!grid[nx][ny])
++cnt;
}
res+=cnt;//融入周长
}
}
}
return res;
}
};
//DFS
class Solution {
public:
int dir[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};
int islandPerimeter(vector<vector<int>> &grid) {
int n = grid.size(), 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) {//找到陆地
ans += DFS(i, j, grid, n, m);
}
if(ans)
break;
}
}
return ans;
}
int DFS(int x, int y, vector<vector<int>> &grid, int n, int m) {
if (x < 0 || x >= n || y < 0 || y >= m || grid[x][y] == 0) {//这一块陆地的边是边界或者紧邻水域
return 1;
}
if (grid[x][y] == 2) {//已经计算过的陆地
return 0;
}
grid[x][y] = 2;//标记陆地为计算过
int res = 0;
for (int i = 0; i < 4; ++i) {
int tx = x + dir[i][0];
int ty = y + dir[i][1];
res += DFS(tx, ty, grid, n, m);//向紧邻的陆地扩散
}
return res;
}
岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
分析
使用DFS解决,一块岛屿只能由上下左右4个方向紧邻的1组成。
那么我们只需要每找到一块陆地,就查探这块陆地的4个方向是否还有陆地,有则继续向4个方向找,没有就返回。
在找到相邻陆地的过程中,我们需要标记找到的陆地,以达到避免重复计算的目的。
code
class Solution {
public:
int dir[4][2] = {{1,0},{0,1},{-1,0},{0,-1}};
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<bool>> visited(m,vector<bool>(n,false));
int res = 0;
for(int i = 0;i<m;++i){
for(int j = 0;j<n;++j)
if(grid[i][j]=='1' && !visited[i][j]){//发现新陆地
++res;//岛屿数量自增
DFS(grid,i,j,m,n,visited);//将与该陆地相连的所有陆地标记
}
}
return res;
}
void DFS(vector<vector<char>>& grid,int x,int y,const int& m,const int&n,vector<vector<bool>>& visited){
visited[x][y] = true;//标记
for(int i = 0;i<4;++i){
int nx = x + dir[i][0];
int ny = y + dir[i][1];
if(nx<m && nx>=0 && ny>=0 && ny<n && !visited[nx][ny] && grid[x][y] == '1')//发现新陆地
DFS(grid,nx,ny,m,n,visited);
}
}
};
岛屿的最大面积
给定一个包含了一些 0 和 1 的非空二维数组 grid 。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
分析
使用DFS解决,计算岛屿的最大面积。
先使用DFS找到一块新陆地,然后沿着新陆地找新陆地,在这过程中计算岛屿面积,每次新岛屿面积计算完成后与当前最大面积比较,大则覆盖,小则不闻不问
code
class Solution {
public:
int dir[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
int maxAreaOfIsland(vector<vector<int>>& grid) {
int area = 0;
int m = grid.size();
int n = grid[0].size();
vector<vector<bool>> visited(m,vector<bool>(n,false));
for(int i = 0;i<m;++i)
for(int j = 0;j<n;++j){
if(grid[i][j] && !visited[i][j]){//新陆地
int cur = 1;//为什么设1?可以好好思索一下
DFS(grid,i,j,m,n,visited,area,cur);//站在新陆地上找相邻的新陆地
}
}
return area;
}
void DFS(vector<vector<int>>& grid,int x,int y,const int& m,const int& n,vector<vector<bool>>& visited,int& res,int& cur){
if(cur>res)//本次岛屿面积最大
res = cur;
visited[x][y] = true;//标记本陆地已被发现过
for(int i = 0;i<4;++i){
int nx = x + dir[i][0];
int ny = y + dir[i][1];
if(nx<0 || ny<0 || nx>=m || ny>=n)//不存在的陆地
continue;
if(grid[nx][ny] && !visited[nx][ny]){//新陆地
++cur;//面积自增
DFS(grid,nx,ny,m,n,visited,res,cur);//继续找
}
}
}
};
统计封闭岛屿的数量
有一个二维矩阵 grid ,每个位置要么是陆地(记号为 0 )要么是水域(记号为 1 )。
我们从一块陆地出发,每次可以往上下左右 4 个方向相邻区域走,能走到的所有陆地区域,我们将其称为一座「岛屿」。
如果一座岛屿 完全 由水域包围,即陆地边缘上下左右所有相邻区域都是水域,那么我们将其称为 「封闭岛屿」。
请返回封闭岛屿的数目。
分析
这波需要反向思考。
要我找封闭岛屿?那就说明存在不封闭的岛屿,不封闭的岛屿具有什么样的特点?
那我们稍微思考一下就能得出结论:不封闭的岛屿是与边界连通的
那我不如先把不封闭的岛屿给处理了,再寻找不封闭的岛屿。
具体实现就是先把不封闭的岛屿给标记了,声明这块岛屿已经被发现过,那么在寻找不封闭的岛屿时,它就根本不会被访问并计算。
code
class Solution {
public:
int dir[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
int closedIsland(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
int res = 0;
vector<vector<bool>> visited(m,vector<bool>(n,false));
//标记不封闭岛屿
for(int i = 0;i<m;++i){
if(!grid[i][0])//第0列
DFS(grid,i,0,m,n,visited);
if(!grid[i][n-1])//最后一列
DFS(grid,i,n-1,m,n,visited);
}
for(int i = 0;i<n;++i){
if(!grid[0][i])//第0行
DFS(grid,0,i,m,n,visited);
if(!grid[m-1][i])//最后一行
DFS(grid,m-1,i,m,n,visited);
}
//寻找封闭岛屿
for(int i = 0;i<m;++i) {
for(int j = 0;j<n;++j){
if(!grid[i][j] && !visited[i][j]){//新陆地
++res;
DFS(grid,i,j,m,n,visited);
}
}
}
return res;
}
void DFS(vector<vector<int>>& grid,int x,int y,const int& m,const int&n,vector<vector<bool>>& visited){
visited[x][y] = true;//标记已被发现
for(int i = 0;i<4;++i){
int nx = x + dir[i][0];
int ny = y + dir[i][1];
if(nx<0 || ny<0 || nx>=m || ny>=n || grid[nx][ny] || visited[nx][ny])//不存在的岛屿or已被发现or不是陆地
continue;
DFS(grid,nx,ny,m,n,visited);
}
}
};
结语
DFS的特点就是一条路走到黑,不见棺材不落泪。该算法往往在实现时会存在一个辅助,帮助DFS避免走过的路重复走,也就是避免重复计算,专业一点称为剪枝。
在使用DFS做题时,我们能剪枝就要剪枝,否则要么会陷入死循环,要么会出现大量重复计算,得不到期望结果。