深度优先搜索模版总结+dfs总结


对于我来说dfs理解起来从感觉怪怪的,听老师讲就听明白,但是拿到一个新题的时候就不太会自己做,所以简单先总结一下dfs的题型及解题方法。

如果一个题没有什么思路就先考虑搜索。

首先对于dfs有两种大模板,第一种是m叉的搜索树dfs直接套的模版,第二种是for循环的模版(我也不知道专业术语,就先这么说吧),相较起来第一种的时间复杂度应该比第二种大。

拿一个例题举个例子

题目描述
从n个数中选择k个数和为sum,求有多少种情况
输入
5 3 9
1 2 3 4 5
输出
2

模版

搜索:m叉dfs模版

现在有n个数,要选择k个数,使得和为sum,求有多少种情况?
我们可以看到对于一个数有两种选择,一是选二是不选,那么自然而然就可以构造一个二叉树。
一层代表一个数的选择情况,左枝表示选,右枝表示不选。
在这里插入图片描述
对应到代码求解的话,在一个dfs中就可以写两个分支,第一个是选的分支,第二个是不选的分支。
那么我们如何确定dfs需要传递的参数呢?由题意可以看到选择k个数,而且和等于sum,所以可以传入三个参数:第一个表示层数(就是判断选择不选择的第几个数),第二个是已经选了几个数,第三个是已经选了的数的和。

那怎么通过找到递归终止的条件呢?易知当选完了k个数并且当所求和满足条件sum的时候我们退出递归,记录找到了一种方案。

代码如下

```cpp
#include <iostream>
using namespace std;
int a[40];
int n,k,sum,ans;
//i表示层数,cnt为个数,s为和
void dfs(int i,int cnt,int s){
    if(i==n){
        if(s==sum&&cnt==k){
            ans++;
        }
        return;
    }
    dfs(i+1,cnt,s);
    dfs(i+1,cnt+1,s+a[i]);
}
int main(){
    cin>>n>>k>>sum;
    for(int i=0;i<n;i++){
        cin>>a[i];
    }
    ans=0;
    dfs(0,0,0);
    cout<<ans<<endl;
    return 0;
}

对于这个模板有几个注意点:
要记录搜索树的层数也就是选择的个数,以及题目所要求的条件,和各种选择的情况。

搜索:for模版

由上一个例题解释一下这个意思:每次选的时候,我们都可以从n个数中选择k个数且保证和为sum。

其实二者的本质都是搜索树!!!!(写文章的时候突然想明白了)只不过实现的方式不太一样!!!时间复杂度的话应该还是第二种比较低,有一个例题可证明。

#include <iostream>
using namespace std;
int a[40];
bool xuan[40];
int n,k,sum,ans;
void dfs(int s,int cnt){
    if(s==sum&&cnt==k){
        ans++;
    }
    for(int i=0;i<n;i++){
        if(!xuan[i]){
            xuan[i]=1;
            dfs(s+a[i],cnt+1);
            xuan[i]=0;
        }
    }
}
int main(){
    cin>>n>>k>>sum;
    for(int i=0;i<n;i++){
        cin>>a[i];
    }
    ans=0;
    dfs(0,0);
    cout<<ans<<endl;
    return 0;
}

对于这个模版需要注意的是:

  1. for循环中要开一个判断是否选择该数的数组,选择之后记得将其标记未选过(依题意情况而定)

递归条件

可行性剪枝

通过一些判断砍掉搜索树上不必要的子树。有时候,我们会发现某个节点对应的子树状态都不是我们要的结果,那么我们其实没必要对这个分支进行搜索,砍掉这个子树,就是剪枝。

一旦发现如果某些状态无论如何都不能找的最终的解,就可以将其进行剪枝。

一般对于可行性剪枝就是依照题意写出退出递归的条件:选择问题就是选择的个数或累加和,迷宫问题就是是否找到出口,连通块问题就是当前的点是否在搜索的图之外。

实现可行性剪枝

/* 从1~30这30个数中选择8个使他的和为200 */
#include <iostream>
using namespace std;
int n=30,k=8,sum=200,ans;
int a[100];
void dfs(int i,int cnt,int s){
    if(cnt>k){    //可行性剪枝
        return;
    }
    if(s>sum){		//可行性剪枝
        return;
    }
    if(i==n){
        if(cnt==k&&s==sum){
            ans++;
        }
        return;
    }
    dfs(i+1,cnt,s);
    dfs(i+1,cnt+1,s+a[i]);
}
int main()
{
    for(int i=0;i<n;i++){
        a[i]=i+1;
    }
    dfs(0,0,0);
    cout<<ans<<endl;
} // namespace std;


实现速度很快。

最优性剪枝

对于最优解的一类问题,通常可以用最优性剪枝,比如在求解迷宫最短路径时,如果发现当前的步数已经超过了当前最优解,那从当前状态开始的搜索都是多余的,因为这样搜索下去都永远找不到更优的解。通过这样的剪枝可以省去大量冗余的计算。

此外,在搜索是否有可行解的过程中,一旦找到了一组可行解,后面所有的搜索都不必再进行了,这算是最优性剪枝的一个特例。(bool类型的变量)

一般对于最优性剪枝,问题会问是否可以找到一组可行解,我们一般会定义一个bool类型的变量,如果找到了一组解将bool设置为true,当其为true时退出递归。

用dfs实现迷宫最短路径

剪枝部分:当步数已经大于答案的时候后面已经没有必要再走了所以进行剪枝

#include <iostream>
using namespace std;
int n,m,ans=1000000;
char maze[100][100];
bool vis[100][100];
int dir[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
bool in(int x,int y){
    return 0<=x&&x<n&&0<=y&&y<m;
}
void dfs(int x,int y,int step){
    if(step>=ans){
        return;
    }
    if(maze[x][y]=='T'){
        ans=step;
        return;
    }
    vis[x][y]=true;
    for(int i=0;i<4;i++){
        int tx=x+dir[i][0];
        int ty=y+dir[i][1];
        if(in(tx,ty)&&!vis[tx][ty]&&maze[tx][ty]!='#'){
            dfs(tx,ty,step+1);
        }
    }
    vis[x][y]=false;
}
int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            cin>>maze[i][j];
        }
    }
    int x,y;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            if(maze[i][j]=='S'){
                x=i,y=j;
            }
        }
    }
    dfs(x,y,0);
    cout<<ans<<endl;
} // namespace std;

重复性剪枝

规定选出的数的位置是递增的,在搜索的时候,用一个参数来记录上一次选取数的位置,那么此次选择我们从这个数之后开始选取,这样最后选出来的方案就不会重复了。

当然,搜索的效率也要比直接二进制枚举更高。

重复性剪枝避免了重复选值。

#include <iostream>
using namespace std;
int a[40];
bool xuan[40];
int n,k,sum,ans;
void dfs(int s,int cnt,int pos){
    if(s>sum||cnt>k){
        return;
    }
    if(s==sum&&cnt==k){
        ans++;
    }
    for(int i=pos;i<n;i++){
        if(!xuan[i]){
            xuan[i]=1;
            dfs(s+a[i],cnt+1,i+1);
            xuan[i]=0;
        }
    }
}
int main(){
    n=30;
    k=8;
    sum=200;
    for(int i=0;i<n;i++){
        a[i]=i+1;
    }
    ans=0;
    dfs(0,0,0);
    cout<<ans<<endl;
    return 0;
}

总之递归条件的出口就是:

选择问题就是选择的个数或累加和看是否满足题意,迷宫问题就是是否找到出口,连通块问题就是当前的点是否在搜索的图之外。

题型+例题

迷宫问题

迷宫解的方案数

连通块问题

踏青-连通块问题
最大蛋糕数-最大连通块问题
蓝桥杯-全球变暖

对于以上三个问题都要用些小技巧解决问题:例如求最大问题的时候,用打擂台的方法一直保存最大的数字;全球变暖问题记录陆地被淹没的数和使用连通块求的数,当二者相等的时候则被淹没。

选择问题

对于选择问题要用一个数组标记是否被选了,之后在主函数的循环中再计数。
引爆炸弹-选择行
8皇后-选择点
等边三角形-选择数
K个数-选择数
正方形-选择数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lydia.na

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值