深度优先搜索(dfs)以及优化

dfs

基本概念

我们已经学习了bfs,他是用来解决从某一个点到另外一个点的最短距离的一种方法。

但我们遇到的搜索问题还有这样一类,就是问你一共有到某个点一共有多少种不同的路径,这样我们的bfs处理起来就会很麻烦,于是我们引入dfs。

对于深度优先搜素,顾名思义,就是搜索的时候优先向深处搜,直到搜到不能再搜我们再一步一步往后回溯。每次搜不到就会往回退一步,由于具有这个特性,他搜索的时候可以全部搜索一遍,而不像bfs一样,第一次到达就截止,由此我们便可以算出有多少种不同的方案。需要注意的是,dfs属于暴搜,当数据较大的时候需要谨慎。

dfs一般利用递归来实现,当我们标记这点开始进行搜索之后,最重要的一个操作便是恢复现场,如下图,往红点开始向右搜,由于经过了红点,于是我们需要将其进行标记,但是如果这样的话,当我们搜到底,需要往回退的是时候,红色点已经走过了,于是左边这条路边不会搜索。这就是为什么我们需要恢复现场的原因

其代码实现的基本思路如下。

void dfs(int t)
{
    if(满足输出条件)
    {
        输出解;
    }
    else
    {
        for(int i=1;i<=尝试方法数;i++)
            if(满足进一步搜索条件)
            {
                为进一步搜索所需要的状态打上标记;
                search(t+1);
                恢复到打标记前的状态;//也就是说的{回溯一步}
            }
    }
}

例题引入 迷宫

#include<bits/stdc++.h>
using namespace std;
const int N=7;
int g[N][N];
bool st[N][N];
int n,m,t,a,b,x,y;//起点,终点
int cnt=0;
int dx[]={-1,0,1,0},dy[]={0,-1,0,1};

void dfs(int x0,int y0)
{
	if(x0==x&&y0==y)
	{
		cnt++;//到终点
		return;//回溯继续搜下一种
	}
	st[x0][y0]=true;
	for(int i=0;i<4;i++)
	{
		int u=x0+dx[i],v=y0+dy[i];
		if(u<1||u>n||v<1||v>m)continue;
		if(st[u][v])continue;
		if(g[u][v]==1)continue;
		st[u][v]=true;
		dfs(u,v);
		st[u][v]=false;//恢复现场,下次回溯搜的时候不会遗漏
	}
			
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m>>t;
	cin>>a>>b>>x>>y;
	while(t--)
	{
		int xx,yy;//障碍物
		cin>>xx>>yy;
		g[xx][yy]=1;//设置障碍
	}
	dfs(a,b);
	cout<<cnt;
	return 0;
}

练习1 迷宫寻路

#include<bits/stdc++.h>
using namespace std;
const int N=105;
char g[N][N];
bool st[N][N];
int n,m;
int dx[]={-1,0,1,0},dy[]={0,-1,0,1};
bool dfs(int x,int y)
{
	if(x==n&&y==m)return true;//到达终点
	st[x][y]=true;//标记走过
	for(int i=0;i<4;i++)
	{
		int u=x+dx[i],v=y+dy[i];
		if(u<1||u>n||v<1||v>m)continue;//越界
		if(st[u][v])continue;//走过就不走了
		if(g[u][v]=='#')continue;//遇到障碍物不走
		if(dfs(u,v))return true;
	}
	return false;
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>g[i][j];
	if(dfs(1,1))cout<<"Yes";
	else cout<<"No";
	return 0;	
}

练习2 八皇后

#include<bits/stdc++.h>
using namespace std;
const int N=30;
int a[N],b[N],c[N],d[N];//行列对角线
int ans;
int n;

void print()
{
	if(ans<=3)
	{
		for(int i=1;i<=n;i++)
			cout<<a[i]<<" ";
		cout<<'\n';
	}
}

void dfs(int pos)
{
	if(pos>n)//搜到终点了
	{
		ans++;
		print();
		return;
	}
	for(int i=1;i<=n;i++)
	{
		if(!b[i]&&!c[i+pos]&&!d[n-i+pos])//如果还没有被占领
		{
			a[pos]=i;
			b[i]=1,c[i+pos]=1,d[n-i+pos]=1;//占领
			dfs(pos+1);//往下一个位置搜
			b[i]=0,c[i+pos]=0,d[n-i+pos]=0;//恢复
		}
	}
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	dfs(1);
	cout<<ans;
	return 0;
}

练习3 单词接龙

#include<bits/stdc++.h>
using namespace std;
const int N=25;
int g[N][N];//i后缀与j前缀的匹配长度
int used[N];//某个单词被用了多少次
string word[N];
int n,ans;

void dfs(string a,int last)
{
	ans=max((int)a.size(),ans);
	
	used[last]++;//第i个单词已经用过了一次
	for(int i=0;i<n;i++)
		if(g[last][i]&&used[i]<2)//last后面可以接i这个单词,并且用了少于2次
			dfs(a+word[i].substr(g[last][i]),i);//拼起来,注意中间重合部分
	used[last]--;
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	for(int i=0;i<n;i++)cin>>word[i];
	char start;cin>>start;//输入开始字母
	//预处理出每个字母的后缀与另外一个字母前缀的匹配长度
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
		{
			string a=word[i],b=word[j];
			for(int k=1;k<min((int)a.size(),(int)b.size());k++)//不能覆盖掉一个单词,取最小值,并且要从1开始,表示有一个重合的
				if(a.substr(a.size()-k,k)==b.substr(0,k))//后缀与前缀相同
				{
					g[i][j]=k;
					break;
				}				
		}
	for(int i=0;i<n;i++)
		if(word[i][0]==start)
			dfs(word[i],i);//当前最后是第i个单词
	
	cout<<ans;
	return 0;
}

练习4 选数

#include<bits/stdc++.h>
using namespace std;
const int N=25;
int a[N];
bool st[N];
int n,k,ans;

bool isprims(int n)
{
	for(int i=2;i*i<=n;i++)
		if(n%i==0)
			return false;
	return true;
}

void dfs(int m,int sum,int pos)
{
	if(m==k)
	{
		if(isprims(sum))
			ans++;
		return;
	}
	for(int i=pos;i<n;i++)
	{
		dfs(m+1,sum+a[i],i+1);//下一次从i+1位置开始取,避免重复计算
	}
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>k;
	for(int i=0;i<n;i++)
		cin>>a[i];
	dfs(0,0,0);
	cout<<ans;
	return 0;	
}

dfs剪枝

背景引入

我们都知道,dfs是一种暴力求解的方法,通过递归的方法求出某一问题的答案,那何为dfs的剪枝?

dfs的剪枝实际上是来优化dfs的时间复杂度的,因为dfs实际是一种暴力,它的时间复杂度非常高,而加了一些剪枝和优化后它的时间复杂度将变为指数级别。

优化的方法

优化剪枝的方法一般有一下几种:

  • 可行性剪枝

  • 最优性剪枝

  • 避免重复冗余

  • 搜索顺序的优化

  • 记忆化搜索(DP,这里不作考虑)

可行性剪枝

实际上就是根据当前搜索到的方案是否合法,如果合法就可以继续搜索

  • 最优性剪枝

当前的情况如果之前有相同的情况被搜索过,那么不继续搜索案,如果当前所搜到的答案已经不具备最优性,那我们就可以不继续搜索。就比如我们现在搜出的答案是4,但是在另外依次搜索中搜出的答案是5,并且还没有搜到完,他搜完也没什么价值了,不会成为我们的答案,只会白白浪费时间。

  • 避免重复冗余

当前的情况如果之前有相同的情况被搜索过,那么不继续搜索

  • 搜索顺序的优化

我们考虑先搜分支较小的点,这样在剪枝的过程中可以尽量少的遍历每一种情况

下面我们通过一道例题来具体阐述

例题 小猫爬山

我们来看两种剪枝优化。首先这是我们根据测试数据按顺序画出的搜索图。其中一种颜色代表一辆车。

如下1,2,12,29装一辆,1994又装一辆。需要两辆车。

最优性剪枝

如下图,我们搜索的时候会出现如下情况,就是每只猫分别装进一辆车,总共需要5辆车,对比上述,其根本不可能成为最优解,即当我们把1994放入第3辆车的时候,已经不可能是最优了。

由于与大的共同装入一辆车的猫选择比较少,于是我们先放入大的就可以减少搜索次数,对比下述,我们发现,都需要两辆车,但是最优解是左边方案,因为左边有一辆车已经装满了,于是还有猫要放入的时候,左边的第一辆我们就根本不需要考虑了,而右边的情况,除了往下搜,我们还得随时注意会不会出现2或者1使得其放入第一辆车。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=20;
int a[N],sum[N];
int n,w,cnt,ans=N;

bool cmp(int x,int y)
{
	return x>y;
}

void dfs(int u)
{
	if(cnt>=ans)return ;//最优化剪枝,比当前答案还差的就不往下搜了
	if(u==n)//边界,猫都被装满了
	{
		ans=cnt;return;
	}
	for(int i=0;i<cnt;i++)//现在有装有猫的车
	{
		if(sum[i]+a[u]<=w)//看看当前猫能放进第几辆车
		{
			sum[i]+=a[u];//装进第i辆车
			dfs(u+1);//下一只猫
			sum[i]-=a[u];//恢复现场
		}
	}
	sum[cnt++]=a[u];//新开一辆车
	dfs(u+1);
	sum[--cnt]=0;//恢复,有可能并不需要新开一辆
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>w;
	for(int i=0;i<n;i++)
		cin>>a[i];
	sort(a,a+n,cmp);//降序排列
	dfs(0);
	cout<<ans;
	return 0;	
}

练习 1小木棍

避免重复冗余

我们主要来看一下剪枝过程,我们我们的剪枝一般在回溯寻找下一种可能的过程中,于是我们主要来看一下在回溯过程中,哪一些情况我们可以直接跳过或者呢。

  1. 对于5 3 3 3 2 2,需要拼接的长度为9,当我们按顺序拼接5和3的时候,长度为8,发现往后继续拼,总会超过9,于是我们需要回溯,回溯到没有选择3的时候,即现在只有5,然后我们选择3的下一个,这时候我们会发现,他后面还是3,那不和刚刚的情况一样了吗,又需要重复上述操作,于是我们选择的剪枝为,直接跳过当前所有的3,于是我们最后拼接的为5+2+2,拼接成功。

  2. 我们看下图,当我们到最底部的时候,发现拼接不成功,我们就会往回退,直到退到2,4,3的位置,这时候我们是把3给解除标记,重新选择,如果选择之后还不能拼接成功,也就是我们所有的数字都试过一遍之后,4还是没有配对的,那我们就会往回退,退到2,0,*的位置,也就是啥都没选的时候,得到这个条件,我们应该直接返回。有同学可能会想,如果4不行,那我换一个数再试试说不定能拼出一根,但是我们刚刚已经遍历完所有然后到,没有与4配对的,所以我们就算再配出很多根,也没什么意义了。于是退到0应该返回不搜了。

3. 我们依然看上图,从1,8,3退回到了1,5,4,这时候我们考虑还要不要换个数字再搜一下,我们知道5是跟3配对的,那要是换其他数去搜,我们能换的数字有哪些呢,是不是只有3或者比3小的,当数字里面有很多个3的时候,我们下一次搜的时候就会搜下一个3.....,但实际上根本没必要,所以这个时候我们直接回溯就好了。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=70;
bool st[N];
int a[N],sum;
int n,len,cnt,cur;//拼接的长度,当前拼接长度可以拼成几根,当前拼接的长度,
bool cmp(int x,int y)
{
	return x>y;
}
//拼到第u根,当前长度为cur,下一根木根为next
void dfs(int u,int cur,int next)
{
	if(u>cnt){cout<<len;exit(0);}//结束标志,让程序退出
	if(cur==len){dfs(u+1,0,1);return;}//需要注意,下一根我们依然要从第一根开始,因为可能会有跳过的情况
	for(int i=next;i<=n;i++)//从下一根开始枚举
	{
		if(st[i]||cur+a[i]>len)continue;
		st[i]=true;
		dfs(u,cur+a[i],i+1);
		st[i]=false;
		//如果能够拼接而成,那么下面代码不会实现,我们需要考虑哪些失败情况往下一定会失败
		if(cur==0)return ;
		if(cur+a[i]==len)return ;
		while(i<n&&a[i]==a[i+1])i++;
	
	}
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;	
	for(int i=1;i<=n;i++)
		cin>>a[i],sum+=a[i];
	sort(a+1,a+1+n,cmp);
	for(len=a[1];len<=sum;len++)//最坏的情况,全部拼接成一根
	{
		if(sum%len)continue;//总的除以每根的长度,有余数那说明,会有剩余的
		cnt=sum/len;//需要拼接几根
		dfs(1,0,1);
	}
	return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值