深度优先搜索DFS总结

1.前言

本篇文章是为了准备蓝桥杯而去写的一篇深度优先搜索DFS的笔记,包括知识点,模板,和例题。

2.DFS简介

一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。属于盲目搜索,最糟糕的情况算法时间复杂度为O(!n)。

3.DFS模板

3.1基本框架

ans;//答案,用全局变量表示
void dfs(层数,其他参数) {           
	if(终止条件判断) {   //到达最底层,或者满足条件退出   
		更新答案;        //更新此时的答案,答案一般用全局变量表示 
		return;         //返回到上一层 
	}
	(剪枝);            //在进一步DFS之前进行剪枝 
	for(枚举下一层可能的情况)  //对每一个情况继续DFS 
		if (used[i]==0) {      //如果状态i没有用过,就可以进入下一层 
			used[i]=1;         //标记状态,表示已经用过,在更底层时不能再使用 
			dfs(层数+1, 其他参数);  //进入下一层 
			used[i]=0;        //恢复状态,回溯时需要重新标记为未用过,不影响上一层对这个状态的使用 
		}
	return;                //返回到上一层 
}

4.注意事项

4.1 DFS的时间复杂度较高,暴力搜索很多时候在数据量大的时候就会tle,因此要注意剪枝(记忆化搜索)
4.2上面的框架是最最简单和最最基本的,有些复杂问题需要做更多的预处理工作和优化以及额外参数的添加。

5.相关例题( 持续更新)

5.1数的划分(⭐⭐⭐⭐)

5.1.1题目描述
将整数n分成k份,且每份不能为空,问有多少种不同的分法。当n=7,k=3时,下面三种分法被认为是相同的:1,1,5;1,5,1;5,1,1
5.1.2输入输出样例
在这里插入图片描述
5.1.3解题思路
本道题关键在于推导出其中的数学公式,这里面相比于模板,参数更多,公式更难推导。但不难看出,若设答案为cnt=F(n, k, now),则F(n, k, now) = F(n - 1, k - 1, 1) + F(n - 2, k - 1, 2) + … + F(n - n / k, k - 1, n / k)。则可以写出递归。

5.1.4源代码

#include <bits/stdc++.h>
using namespace std;
#define Max 100
int cnt; 
void dfs(int n, int k, int now) {
    // now表示当前至少从多少开始分,例如第一个应该是从1开始分
    if(k == 1) // 1份不用分了
        cnt++;
    else {
        // F(n, k, now) = F(n - 1, k - 1, 1) + F(n - 2, k - 1, 2) + ... + F(n - n / k, k - 1, n / k)
        for(int i = now; i <= (n / k); i++)
            dfs(n - i, k - 1, i); // 递归:把n-i分为k-1份,必须从i开始分
    }
}
int main() {
    int n, k;
    cin >> n >> k;
    dfs(n , k, 1);
    cout << cnt << endl;
    return 0;
}

5.1.5总结
本道题关键在于数学公式的推导。

5.2小木棍(⭐⭐⭐⭐⭐)

5.2.1题目描述
德莱文有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过50。现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。
5.2.2输入输出
在这里插入图片描述
5.2.3解题思路
1)首先,木棍长度小于50,原始输入数据中包含有超过50的,因此要过滤掉
2)在dfs外围进行枚举,但要注意优化
3)优化1:在木棍拼接时,通过从长到短的顺序去试木棍,因为更小的木棍可以更灵活的拼接, 需要试验的次数就更少
4)优化2:根据优化1,将输入的木棍从大到小排好序后,当用木棍i拼合原始长棍时,从第i+1根木棍开始往后搜。
5)优化3:当dfs返回拼接失败,需要更换当前使用的木棍时,不要再用与当前木棍的长度相同的木棍,因为当前木棍用了不行,改成与它相同长度的木棍一样不行。这里我预处理出了排序后每根木棍后面的最后一根与这根木棍长度相等的木棍(程序中的next数组),它的下一根木棍就是第一根长度不相等的木棍了。
6)优化4:只找木棍长度不大于未拼长度rest的所有木棍。
7)优化5:由于是从小到大枚举 原始长度,因此第一次发现的答案就是最小长度。dfs中只要发现所有的木棍都凑成了若干根原长度的长棍(容易发现 凑出长棍的根数=所有木棍的长度之和/原始长度),立刻一层层退出dfs,不用滞留,退到dfs外后直接输出原始长度并结束程序。
8)优化6:还有一个难想却特别特别重要的优化:如果当前长棍剩余的未拼长度等于当前木棍的长度或原始长度,继续拼下去时却失败了,就直接回溯并改之前拼的木棍。
5.2.4源代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=75;
int a[N],sum,r,d,n,mx;
int nxt[N];
bool vis[N];
bool dfs(int k,int la,int l)//k是当前这在拼的木棍的编号,la是上一根木棍的编号,l是当前这根木棍剩余的长度 
{
    if(k==r+1)//已经完成了一直到r号编号的的木棍的拼接,符合题意 
        return 1;
    for(int i=la+1;i<=n;)//最外层循环 
    {
        if(vis[i])
        {
            i++;
            continue;
        }
        if(l+a[i]<=d)//优化4:要求当前正在拼的木棍的长度+剩余长度<原始长度 
        {
            vis[i]=1;//标记为已使用过 
            if(l+a[i]<d&&dfs(k,i,l+a[i]))//当前木棍长度+剩余长度<原始长度并且下一根木棍拼接成功,则拼接成功 
                return 1;
            if(l+a[i]==d)//当前木棍长度+剩余长度=原始木棍长度,说明该编号的木棍拼接完成 
            {
                if(dfs(k+1,0,0))//进入下一个编号的木棍开始拼接,拼接成功,则返回1 
                    return 1;
                //vis[i]=0;//优化6:否则,下一个编号的木棍拼接不成功,说明失败了则需要进行回溯 
                //return 0;
            }
            vis[i]=0;
        }
        i=nxt[i];//否则使用下一根长度不同的木棍,并进入循环 
    }
    return 0;
}
bool cmp(int x,int y)//升序函数 
{
    return x>y;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        if(a[i]>50)//自动过滤掉大于50的数 
        {
            i--;
            n--;
        }
        else
        {
            mx=max(mx,a[i]);//找到最大长度 
            sum+=a[i];
        }
    }
    sort(a+1,a+n+1,cmp);//优化1 
    for(int i=1;i<=n;i++)//优化3:nxt数组的作用在于预处理一根木棍下一根和他长度不通的木棍的位置 
    {
        int flag=0;
        for(int j=i+1;j<=n;j++)
            if(a[j]!=a[i])
            {
                flag=1;
                nxt[i]=j;
                break;
            }
        if(!flag)
            nxt[i]=n+1;
    }
    for(r=n;r>0;r--)//原始木棍的总个数从n开始递减,要能够被总和整除 
    {
        if(sum%r==0)
        {
            d=sum/r;//每一根原始木棍的长度 
            if(d<mx)continue;//如果比最大长度小,不符合题意,重新寻找 
            if(dfs(1,0,0))//优化5:第一次符合题意的d,就是最小d 
                break;
        }
    }
    cout<<d<<endl;
    return 0;
}

5.2.5总结
本道题中,关键在于dfs的优化,其中有几条优化特别难想,但是不一定非要写上去,有些不必要的优化即使不写也不会超时,但有一些必须的优化是一定要有的(比如剪枝)。

5.3一帆风顺(⭐⭐⭐)

5.3.1题目描述
在这里插入图片描述
5.3.2输入输出样例
在这里插入图片描述
5.3.3解题思路
在进行dfs的时候设置三个参数:当前位置x,当前食物的分数sum,当前补充食材的次数k。递归终止的条件是x到达n,同时分情况考虑增加食物的情况和不增加食物的情况。
5.3.4源代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int a[N],b[N];
int mx=-1;
int n;
void dfs(int x,int sum,int k) {
	if(x==n) {
		mx=max(mx,sum);
		return ;
	}
	if(sum<b[x]) return ;
	if(k<3) dfs(x+1,sum+a[x+1]-b[x],k+1); //搜索增加食物的情况
	dfs(x+1,sum-b[x],k); //搜索放弃食物的情况
}
int main() {
	cin>>n;
	for(int i=1; i<=n; i++) cin>>a[i];
	dfs(0,0,0);
	cout<<mx<<endl;
}

5.3.5总结
一个简单的搜索,注意只有两次补充食物的机会即可。

5.4.危险系数(⭐⭐⭐⭐)

5.4.1题目描述
在这里插入图片描述
5.4.2输入输出样例
在这里插入图片描述

5.4.3解题思路
很明显的DFS搜索图的问题,采用邻接矩阵建图。这里最关键的是怎么去搜索,我这里采用的搜索方法是搜索出所有路径,每搜索出一条路径ans++。同时,对应的这条路径上的每个点都相应的+1。最后DFS搜索完成后,如果对应的b[搜索过的点下标]==ans,就说明该点是每一条路径都要经过的点,即为关键点。最后统计出这样的点有多少个即可。(注意剪枝)
5.4.4源代码

#include<bits/stdc++.h>
using namespace std;
#define maxn 1005
int n,m,u,v,ans;
int a[maxn][maxn];
int vis[maxn];
int b[maxn];

void DF(int u,int v) {
	if(u==v) {
		ans++;
		for(int i=1; i<=n; i++)//将路径上的每个点都+1
			if(i!=u && i!=v && vis[i]) b[i]++;
		return;
	}
	for(int i=1; i<=n;i++) {
		if(a[u][i]==1 && !vis[i]) {
			vis[u]=1;//标志这个点已经被访问过了
			DF(i,v);
			vis[u]=0;//回溯的时候将走过的点标志为未走过,否则到达不了终点
		}
	}
}

int main() {
	cin>>n>>m;
	while(m--) {
		int i,j=0;
		cin>>i>>j;
		a[i][j]=1;
		a[j][i]=1;
	}
	cin>>u>>v;
	DF(u,v);
	int cnt=0;
	if(!ans) {//若两点之间无通路,则返回-1
		cnt=-1;
	} 
	else {
		for(int i=1; i<=n; i++)
			if(i!=u && i!=v && b[i]==ans)
				cnt++;//统计有多少个关键点
	}
	cout<<cnt<<endl;

	return 0;
}

5.4.5总结
一道经典的图论中的DFS搜索问题,思路上稍微有些改变,其他都是老一套。

5.5网络寻路(⭐⭐⭐⭐)

5.5.1.题目描述
在这里插入图片描述
5.5.2.输入输出
在这里插入图片描述

在这里插入图片描述
5.5.3解题思路
1.首先明确,题目中的目的地有两种,一种是回到原点,一种是到达没有到达的地方
2.在路径中经过的点不能有重复的点
3.有题目可知,4 1 2 3和3 2 1 4是两条不同的路径
4.给出题目中样例的图示
在这里插入图片描述
5.使用vis[]数组记录经过的点;
6.使用DFS寻找可能的路径,因为路径的长度是4,那么当寻找路径上的前3个点的时候,如果可以从前一个点走到当前的点,并且当前的点没有走过,那么将当前的点设置为路径当中的点。
7.寻找路径第四个点的时候,有两种可能的情况,一种是可以到达的第四个点是之前没有走过的点,方案数量加一,另外一的情况是可以到达的第四个点是第一个点(走回到了起点),方案数量加一。
8.建图时采用邻接表而不是邻接矩阵,防止超时
5.5.4源代码

#include<bits/stdc++.h>
using namespace std;
int M,N,u,v,ans;
#define maxn 10010
vector<int>G[maxn];
bool used[maxn];
 
//u表示上一个顶点,dep表示当前寻找第dep+1个结点,s表示起点; 
void DFS(int u,int dep,int s){
    if(dep==3){ 
        for(int i=0;i<G[u].size();i++){
            int v=G[u][i];
            if(!used[v] || v==s) ans++; 
        }
        return ;
    }
    else{
        for(int i=0;i<G[u].size();i++){
            int v=G[u][i];
            if(!used[v]){
                used[v]=true;
                DFS(v,dep+1,s);
                used[v]=false;
            }
        }
    }
    return ;
}
 
int main(void){
    cin>>N>>M;
    while(M--){
        cin>>u>>v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
     
    memset(used,0,sizeof used);
    for(int i=1;i<=N;i++){
        used[i]=true; 
        DFS(i,1,i);
        used[i]=false; 
    }
    cout<<ans;
    return 0;
}

5.5.5总结
DFS更直接更暴力更容易去想,但是写起来代码有点长,而且需要对DFS非常熟悉才行,并且存在超时的可能性

5.6生日蛋糕

5.6.1问题描述
在这里插入图片描述
5.6.2输入输出
在这里插入图片描述
5.6.3解题思路
预处理出每一层的最小体积和最小侧面积
从底层m向上层搜索至第0层停止搜索,枚举每一层可能的高度和半径,判断是否合法,合法则继续搜索上一层。

需要剪枝的情况:
1.上层最小体积+目前体积>目标体积
2.上层最小侧面积+目前面积>现有答案
3.剩下的侧面积+目前面积>现有答案
5.6.4AC代码

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
int m,n,mins[30],minv[30],ans=(int)1e9,h[30],r[30];
int s=0,v=0;
void dfs(int dep) {
	if(dep==0) {
		if(v==n) ans=min(ans,s);
		return;
	}
	for(r[dep]=min(r[dep+1]-1,(int)sqrt(n-v)); r[dep]>=dep; r[dep]--) {
//sqrt(n-v):剩余体积做成一层的半径,r[dep+1]-1:上一层至少比下一层小1,
//		r[dep]>=dep:每一层半径需比本层层数大,否则一共不够m层
		for(h[dep]=min((int)((double)(n-v)/r[dep]/r[dep]),h[dep+1]-1); h[dep]>=dep; h[dep]--) {
//剩余体积做成的一层高度,至少比下一层小1,保证能构成m层
			if(v+minv[dep-1]>n) continue;//上层最小体积+目前体积>目标体积
			if(s+mins[dep-1]>ans) continue;//上层最小面积+目前面积>现有答案,不是最优
			if(s+(double)2*(n-v)/r[dep]>ans) continue;//2*(n-v)/r[dep]:用体积确定剩
			//下的侧面积+目前面积>现有答案,不是最优
			if(dep==m) s+=r[dep]*r[dep];//从下往上第一层加上上表面面积,其它层只需考虑侧
			//面积
			v+=r[dep]*r[dep]*h[dep];
			s+=2*r[dep]*h[dep];
			dfs(dep-1);
			if(dep==m) s-=r[dep]*r[dep];//还原
			v-=r[dep]*r[dep]*h[dep];
			s-=2*r[dep]*h[dep];
		}
	}
}
int main() {
	cin>>n>>m;
	mins[0]=minv[0]=0;
	for(int i=1; i<=m; i++) { //每一层最小r、h均是层数,预处理算出每层最小s,v,逐个递增
		mins[i]=mins[i-1]+2*i*i;
		minv[i]=minv[i-1]+i*i*i;
	}
	h[m+1]=r[m+1]=(int)1e9;
	dfs(m);
	cout<<ans<<endl;
	return 0;
}

5.7砝码称重(DFS不完全正确)

5.7.1问题描述
在这里插入图片描述
5.7.2输入输出
在这里插入图片描述
5.7.3解题思路
首先需要额外开一个数组c[10005],下标为总重量,值为0或1,代表该重量是否可行。
定义一个dfs的参数有两个:i表示当前砝码序号,sum表示当前砝码重量总和。
循环结束条件:i==n,此时判断sum若不等于0(根据题意),则sum重量是可以被称出的。
递归:对于第i个砝码,有三种情况,减去当前重量,加上当前重量,或者不使用这个砝码。
最后,统计c数组中有多少个元素值为1即可。
5.7.4源代码(注意:改代码不能完全ac该题,只能拿到一半的分数,但是蓝桥杯嘛,不要求全对,能拿到大部分分数就可以了,正确的动态规划解法在另一个dp笔记总结里面)

#include<bits/stdc++.h>
using namespace std;
#define maxn 105
int a[maxn],n,c[100005],cnt=0,vis[maxn];
void dfs(int i,int sum){//i表示当前砝码的序号,w表示当前砝码的总质量
if (i == n) {
	if(sum) c[abs(sum)] = 1;//表示该重量可以,将数组置为1 
	return;
	}
	if(!vis[i]){
	vis[i]=1;
	dfs(i+1,sum+a[i]);//加上第x个砝码
	dfs(i+1,sum);//不加第x个砝码
	dfs(i+1,sum-a[i]);//减去第x个砝码
}
vis[i]=0;
}
int main(){
	cin>>n;
	for(int i=0;i<n;i++) cin>>a[i]; 
	dfs(0,0);
	for(int i=0;i<=100000;i++){
		if(c[i]==1) cnt++;
	} 
	cout<<cnt;
return 0;
}

6.总结

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

chase__young

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

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

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

打赏作者

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

抵扣说明:

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

余额充值