【算法学习系列第一章】 深度优先遍历(DFS)思路及经典例题讲解

DFS算法简介

本文致力于从是什么,为什么,怎么做三个方面解决在算法题中出现的与DFS有关的习题
dfs算法是一种搜索算法,主要思想就是从某一个分支的起始节点开始遍历,尽可能的往深处走,以求把该条分支遍历完。有关DFS的原理可以看这篇博客

1.什么时候需要用到DFS

一般来说,DFS用于暴搜,连通性问题,求解符合条件的方案数,最大值,最小值。
一般地,能用BFS一定可以用DFS,但是能用BFS不一定能用DFS

2.DFS如何用

一般来说,DFS解题思路主要分为以下几个步骤:
(1) find 在当前层横向遍历,找到符合题中所述条件的节点;这一步也称之为剪枝
(2) forward 如果在当前层找到了符合条件的节点,并且当前层不是最后一层,就把节点加入到当前层。跳到find
(3) done 如果在当前层找到了符合条件的节点,并且当前层是最后一层,就按照题目要求输出答案。跳到find
(4) back 在当前层没有找到符合条件的节点,,返回上一层当前节点的下一个节点。跳到find

有关DFS的经典例题及其解题思路讲解

1.DFS求解全排列问题

全排列问题可以用C++STL模板库里面的algorithm头文件里面的next_permutation函数。具体用法可以百度。
问题:为什么本题需要定义st数组?因为本题属于外部搜索,是把整个数组看成一个整体,问能否从一个状态转化成另一个状态,所以需要。

#include<iostream>

using namespace std;
const int N=10010;
int n;
int a[N];
bool st[N];
void dfs(int k)
{
	if(k==n)//如果找到符合条件的节点,并且当前层是最后一层,这一步叫done
	{
		for(int i=0;i<n;i++)
		{
			cout<<a[i]<<" "; 
		}
		cout<<endl;
		return ;
	}
	for(int i=1;i<=n;i++)//在当前层横向遍历,尝试找到符合条件的节点find
	{
	//找到符合条件的节点,并并且当前层不是最后一层,就把节点加入当前层forward
		if(!st[i])
		{
			st[i]=true;//如果未遍历过,就遍历 
			a[k]=i;
			dfs(k+1);
			st[i]=false;//相当于是在几个房间里依次找东西的时候,在一个房间里面找完之后在进到下一个房间找的时候需要把该房间收拾一下,使其恢复原状
			//这一步也叫作回溯    back 
		}
	}
}
int main()
{
	cin>>n;
	dfs(0);
	
	
	return 0;
}

类似的,关于全排列还有如下几道经典例题:
递归实现指数型枚举
在这里插入图片描述
这里采用一个布尔数组记录每个数是否选了。为什么要用?原理同上。相应的,每一个数对应着选与不选两种状态
思路即代码。

#include<iostream>

using namespace std;
int n;
const int N=20;
bool st[N];
void dfs(int k){
    if(k>n){
        for(int i=1;i<=n;i++){
            if(st[i]){
                cout<<i<<" ";
            }
        }
        cout<<endl;
        return;
    }
    st[k]=true;//选第k个数
    dfs(k+1);
    st[k]=false;//不选第k个数
    dfs(k+1);
}
int main(){
    cin>>n;
    dfs(1);

    return 0;
}

2.DFS求解联通块问题

例题:全球变暖

在这里插入图片描述
这里思路就是说在用DFS求解连通块的时候用两个变量记录一下陆地的数目和岛屿里面边界的数目,因为边界会逐渐被淹没。本题属于外部搜索,所以需要st数组。

#include<iostream>

using namespace std;
const int N=1010;
int n;
char g[N][N];
int ans;
bool st[N][N];
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};

void dfs(int x,int y,int& total,int& bound)
{
    
	st[x][y]=true;//将新节点加入进去
	total++;
	bool isbound=false;
	for(int i=0;i<4;i++)//寻找ok的节点,在当前层横向遍历 
	{
		int nowx=x+dx[i],nowy=y+dy[i];
		//在当前层进行操作 
		if(nowx<0||nowx>=n||nowy<0||nowy>=n) continue;//如果坐标非法 
		if(st[nowx][nowy]) continue;//如果当前这个点已经走过, 
		if(g[nowx][nowy]=='.')//如果该点的周围有海 
		{
			isbound=true;//是边界 
			continue;
		}
		//如果找到或者当前层的点进行完,进入当前层节点的兄弟节点 
		dfs(nowx,nowy,total,bound);
	}
	if(isbound) bound++;
 } 
int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
	{
		scanf("%s",g[i]);
	}
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			if(!st[i][j]&&g[i][j]=='#')
			{
				int total=0,bound=0;
				dfs(i,j,total,bound);//搜索岛屿的数量和某个岛屿中 
				if(total==bound)ans++;//统计被淹没的岛屿数量 
			}
		}
	}
	cout<<ans<<endl; 
	return 0;
}

另外这个题还有另外一种思路,就是搜索时每遇到一块陆地并且这块陆地之前没有搜过就遍历这块陆地周围的陆地并且把它们都打上标记,岛屿数就加一,继续搜下一块没有遍历过的陆地,至于被淹没的岛屿数,利用搜索的时候遍历陆地时如果该陆地的上下左右四个方向上都是陆地,那么被淹没的岛屿数就加一。

#include<iostream>

using namespace std;
const int N=1010;
int n,total=0;
char g[N][N];
bool st[N][N];
int flag;
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};
void dfs(int x,int y){
    st[x][y]=true;
    if(g[x][y+1]=='#'&&g[x+1][y]=='#'&&g[x][y-1]=='#'&&g[x-1][y]=='#'){
        flag=1;//上下左右都是陆地,不会被淹没
    }
    for(int i=0;i<4;i++){
        int tx=x+dx[i],ty=y+dy[i];
        if(st[tx][ty]==false&&g[tx][ty]=='#'){

            dfs(tx,ty);
        }
    }

}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>g[i][j];
        }
    }


    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){

            if(g[i][j]=='#'&&st[i][j]==false){
                flag=0;
                dfs(i,j);//搜索一片陆地
                if(flag==0){//如果该陆地的中么有一个点是上下左右都是陆地的
                    total++;//被淹没的岛屿数量加一
                }

            }

        }
    }
    cout<<total<<endl;

    return 0;
}

联通块问题经典的还有岛屿的最大面积
在这里插入图片描述
思路就是遍历一遍数组,更新岛屿的最大面积,同时为了不让岛屿被重复搜索,如果当前这个点是陆地,就把陆地变成0标记已经被搜索过了,然后继续向下搜索。这里没有必要定义数组,每次搜到陆地就标记为0就行了,属于外部搜索。
代码如下:

class Solution {
    int dfs(vector<vector<int>>& grid, int x, int y) {
        if (x < 0 || y < 0 || x == grid.size() || y == grid[0].size() || grid[x][y] != 1) {
            return 0;
        }
        //默认坐标合法
        //默认grid[x][y]一定是陆地
        grid[x][y] = 0;
        int di[4] = {0, 0, 1, -1};
        int dj[4] = {1, -1, 0, 0};
        int ans = 1;
        for (int i = 0; i < 4; i++) {
            int tx = x + di[i], ty = y + dj[i];
            ans += dfs(grid, tx, ty);
        }
        return ans;
    }
public:
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int ans = 0;
        for (int i = 0; i < grid.size(); i++) {
            for (int j = 0; j < grid[0].size(); j++) {
                ans = max(ans, dfs(grid, i, j));
            }
        }
        return ans;
    }
};

3.DFS求解方案数

例题:红与黑
在这里插入图片描述该题需要注意的一点是输入的时候h表示的是行数,而w表示的是列数。
思路:首先每一个格子表示一个状态,利用DFS搜索整个地图,如果
当前所在的格子地板是黑色,就ans++,接着继续往下搜。最后输出ans。

代码如下:

#include<iostream>
#include<queue>
using namespace std;
const int N=25;
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};
int w,h,ans=1;
char g[N][N];
bool st[N][N];
void dfs(int x,int y)
{
	st[x][y]=true;//标记一下,表示遍历过
	for(int i=0;i<4;i++)//在当前层横向遍历,尝试找到符合条件的节点   find
	{
		int nowx=x+dx[i],nowy=y+dy[i];
		if(nowx<0||nowx>=h||nowy<0||nowy>=w) continue;
		if(st[nowx][nowy]) continue;
		if(g[nowx][nowy]=='#') continue;
		if(g[nowx][nowy]=='.')//如果找到了ok的节点,并且当前层不是最后一层
		{
			ans++;
			dfs(nowx,nowy);//继续搜下一个节点
			
		 } 
	}
}
int main()
{
	cin>>w>>h;//h行和W列 
	for(int i=0;i<h;i++) scanf("%s",g[i]);
	for(int i=0;i<h;i++)
	{
		for(int j=0;j<w;j++)
		{
			if(g[i][j]=='@')//寻找起始位置,并从这个点开始搜
			dfs(i,j);
		}
	}
	cout<<ans<<endl;
	
	return 0;
 } 

迷宫问题

#include<iostream>

using namespace std;
const int N=10;
int n,m,t;
int a[N][N];//地图,0表示无障碍,1表示有障碍
int sx,sy,ex,ey;
bool st[N][N];
int ans;
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};
bool check(int x,int y){
    if(x<=0||x>n||y<1||y>m) return false;
    if(st[x][y]) return false;
    if(a[x][y]) return false;
    return true;
}
void dfs(int x,int y){
    if(x==ex&&y==ey){
        ans++;
        return;
    }
    st[x][y]=true;
    for(int i=0;i<4;i++){
        int tx=x+dx[i],ty=y+dy[i];
        if(check(tx,ty)){
            dfs(tx,ty);
        }
    }
    st[x][y]=false;
}
int main(){
    cin>>n>>m>>t;
    cin>>sx>>sy>>ex>>ey;
    while(t--){
        int x,y;
        cin>>x>>y;
        a[x][y]=1;
    }
    dfs(sx,sy);
    cout<<ans<<endl;
    return 0;
}

另外,值得注意的是,在求解方案数的时候,有时DFS暴力枚举会超时无法全部AC,比如下面这道题
地宫取宝
在这里插入图片描述

首先这里先用暴力枚举的DFS做法,这种只能过一半数据,也是一种思路。这个题有个坑,就是说在走到终点的时候,有可能手中的宝贝数正好为k,但是也有可能正好为k-1,在满足终点格子的宝贝价值大于手中的任意宝贝价值的时候,也是一种合法的方案。
另外这个题为什么不用vis数组(记录每个点是否访问过)呢?
本题属于内部搜索,因为本质是从一个点走到另外一个点,而不是把整个地图当成一个状态,问你能否从这个状态变成最终状态。因此不需要vis数组。
代码如下:

#include<bits/stdc++.h>

using namespace std;
//1436: 蓝桥杯2014年第五届真题-地宫取宝
//每一次拿起宝贝就更新一下最大值
typedef long long LL;
LL ans;
int n,m,k;
const int N=110,p=1e9+7;
int a[N][N];//存储每一个格子的宝贝价值
//x,y 右x,y+1  下x+1,y
int dx[]={0,1};
int dy[]={1,0};
void dfs(int x,int y,int cnt,int maxv){//从0,0开始走,拿了cnt件宝贝,当前宝贝的最大值为maxv
    if(x==n-1&&y==m-1){
        if(cnt==k||cnt==k-1&&a[x][y]>maxv){
            ans=(ans+1)%p;
        }
        return;
    }
    for(int i=0;i<2;i++){
        int tx=x+dx[i],ty=y+dy[i];
        if(tx<n&&ty<m){
           if(a[x][y]>maxv){
               dfs(tx,ty,cnt+1,a[x][y]);
           }
           dfs(tx,ty,cnt,maxv);
        }

    }
}
int main(){
    cin>>n>>m>>k;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
             cin>>a[i][j];
        }
    }
    dfs(0,0,0,-1);//因为宝贝的价值范围大于等于0,所以这里最大值初始化为-1
    cout<<ans<<endl;
    return 0;
}

有关记忆化搜索的问题会在以后单独写一个章节。

4.DFS求解八皇后问题

八皇后
八皇后问题是用DFS求解的一个经典问题。其中对于算法中的坐标表示有不理解的小伙伴可以看一下这一篇题解,讲解的非常透彻!!!

首先,算法思路是枚举第i行时,搜索如果第i列,该点所在的对角线没有皇后,那么就可以放置皇后,否则就回溯。
用四个数组分别表示行,列,对角线。
因为本题中每一种方案执行前都需要重新回复现场,所以需要数组来记录该列,对角线是否有皇后。
代码如下:

#include<iostream>

using namespace std;
const int N=100;
int a[N];//输出数组
int n,ans;
int h[N],l[N],vl[N],vr[N];
void dfs(int k){//第k行
    if(k==n+1){
        if(ans<=2){
            for(int i=1;i<=n;i++) cout<<a[i]<<" ";
            cout<<endl;
        }
        ans++;
        return;
    }
    for(int i=1;i<=n;i++){//第i列
        if(!h[i]&&!vl[i+k]&&!vr[i-k+n]){
            a[k]=i;//第k个皇后的列号是 i
            h[i]=vl[i+k]=vr[i-k+n]=1;
            dfs(k+1);
            h[i]=vl[i+k]=vr[i-k+n]=0;

        }
    }
}
int main(){
    cin>>n;
    dfs(1);//从第一行开始搜索
    cout<<ans<<endl;
    return 0;
}

2n皇后问题

请添加图片描述
本题主要思路是,在枚举每一个黑皇后能放的位置的时候,再进行一次搜索白皇后所在的位置,注意此题的回溯写法。还有坐标不要搞混了。

#include<iostream>

using namespace std;
const int N=110;
int a[N][N];
int n;
int ans;
int yhei[N],vlhei[N],vrhei[N];//黑皇后的列数组,对角线数组
int ybai[N],vlbai[N],vrbai[N];//白皇后的列数组,对角线数组
void dfs(int k){//第k行
    if(k==n+1){
        ans++;
        return ;
    }
    for(int j=1;j<=n;j++){//第j列
        if(!yhei[j]&&!vlhei[k-j+n]&&!vrhei[k+j]&&a[k][j]){
            yhei[j]=vlhei[k-j+n]=vrhei[k+j]=1;
            a[k][j]=0;
            for(int i=1;i<=n;i++){
                if(!ybai[i]&&!vlbai[k-i+n]&&!vrbai[k+i]&&a[k][i]){
                    ybai[i]=vlbai[k-i+n]=vrbai[k+i]=1;
                    a[k][i]=0;//注意题中0不能放皇后
                    dfs(k+1);
                    a[k][i]=1;//可以放皇后
                    ybai[i]=vlbai[k-i+n]=vrbai[k+i]=0;
                }
            }
            a[k][j]=1;
            yhei[j]=vlhei[k-j+n]=vrhei[k+j]=0;
        }
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            cin>>a[i][j];
    dfs(1);//从第一行开始搜
    cout<<ans<<endl;
    return 0;
}

5.求解最小值

一般来说,数据范围不是很大的时候DFS可以求解至少需要多少次之类的问题,当然他的时间复杂度比较高,不如动态规划。
例题:奇怪的电梯
注意递归函数的变量。一个是当前所处层,一个是当前按钮按了多少次
算法思路就是当搜索到第k层,按钮按了cnt次的时候,如果向上不超范围,就向上走,如果向下不超范围,就向下走。注意回溯的处理办法

#include<iostream>

using namespace std;
const int N=210;
int n,a,b;
int ans=0x7ffffff;
int k[N];
bool st[N];
void dfs(int step,int cnt){//搜到第step层,按了cnt次按钮
    if(step==b) ans=min(ans,cnt);
    if(cnt>ans) return;//按钮数大于答案,说明找不到
    st[step]=1;//标记
    if(step+k[step]<=n&&!st[step+k[step]]) dfs(step+k[step],cnt+1);
    if(step-k[step]>=1&&!st[step-k[step]]) dfs(step-k[step],cnt+1);
    st[step]=0;//回溯
}
int main(){
    cin>>n>>a>>b;
    for(int i=1;i<=n;i++) cin>>k[i];
    dfs(a,0);
    if(ans!=0x7ffffff) cout<<ans<<endl;
    else cout<<"-1"<<endl;
    return 0;
}

我们可以看到DFS在解题中的应用,这些都是些经典例题,奈何本人能力有限,学DFS还是要以具体题目为主。在题目中体会DFS的思想是如何应用的。

希望能够对各位小伙伴有帮助,如果觉得还可以可以关注一下么,谢谢啦(手动狗头)。

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值