广度优先搜索和深度优先搜索

图论学的破防了,于是先来恶补搜索。

一、深度优先搜索(DFS)

        是一种利用函数递归枚举的方式,先找到一个初始点然后枚举可行的方向,如果满足条件,那么就继续搜索,如果不满足条件就返回去寻找一个上一个可行的节点,继续枚举。

        这种方式称为回溯算法,也称为深度优先搜索。是一种不撞南墙不回头的算法。

        模板是这样的:

void dfs(int k){//k代表递归层数,或者说要填几个空
    if(所有空已经填完了){
        判断最优解/记录答案;
        return;
    }
    
    for(枚举这个空能填的选项){
        if(这个选项合法){
            记录下这个空;
            dfs(k+1);
            取消这个空;    
        }
    }
}

dfs思考的几个点:要传几个参,为什么要传参。而这个参数一定是需要保存的信息,也和结束条件是息息相关的。还有什么时候return,关系到程序的出口。

举几个例子:

P1219 [USACO1.5] 八皇后 Checker Challenge - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include <iostream>
using namespace std;
long long int ans=0;
int a[100];//存当前的排列 
int b[100];//纵行
int c[100];//x+y;
int d[10000];//x-y+n*n;
int n; 
void dfs(int x)
{
	if(x>n)
	{
		ans++;
		if(ans<=3)
		{
			for(int i=1;i<=n;i++)
			{
				cout<<a[i]<<" ";
			}
			cout<<endl;
		}
		return;
	}
	
	for(int i=1;i<=n;i++)//i如果是横行的话,那么x/n就是纵行, b[i]代表x=i这一列已经有人来过 
	{
		if(b[i]==0&&c[i+x]==0&&d[i-x+n*n]==0)
		{
			a[x]=i;
			b[i]=1;c[i+x]=1;d[i-x+n*n]=1;
			dfs(x+1);
			b[i]=0;c[i+x]=0;d[i-x+n*n]=0;
		}
	}
}
int main()
{
	cin>>n;
	dfs(1);
	cout<<ans<<endl;
	return 0;
}

这个问题就是能填多少个互相不去攻击的皇后。

那我们怎么传参呢?由于问题是求放置皇后,那就设x是皇后的数量,接下来看是否需要其他条件判断一个放置是否满足条件。由于每一行只能放一个皇后,可以把x当作当前正在处理行,枚举就可以了。

结束条件就是放置皇后的个数大于需要的个数。

判断条件是否合适是这道的小难点,就是用三个数组vis看所占据位置,一个存纵坐标,两个存斜的坐标,难点在于建立位置x和两条斜的坐标线。只要建立每个斜着的方格到斜坐标的一一映射就可以了。

 P2392 kkksc03考前临时抱佛脚 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include <iostream>
#include <algorithm>
using namespace std;
int a[4];
int s[21];
int sum=0;
int sum1=0;
int maxdeep,nowtime,maxtime;
void dfs(int x)
{
	if(x>=maxdeep)
	{
		maxtime=max(maxtime,nowtime);
		return;
	}
	
	if(s[x]+nowtime<=sum/2)
	{
		nowtime+=s[x];
		dfs(x+1);
		nowtime-=s[x];
	}
	dfs(x+1);
}
int main()
{
	cin>>a[0]>>a[1]>>a[2]>>a[3];
	for(int i=0;i<4;i++)
	{
		maxdeep=a[i];
		for(int j=0;j<a[i];j++)
		{
			cin>>s[j];
			sum+=s[j];
		}
		dfs(0);
		sum1+=sum-maxtime;
		sum=0;nowtime=0;maxtime=0;
	}
	cout<<sum1<<endl;
	return 0;
}

这个题找最短时间,先发现了四个集合是相互独立的,于是dfs四次。每个集合内寻找一组数字:即这一组时间的加起来的总和小于总时间的一半。在这些满足条件的组里面寻找最大值。

怎么传参?传入一个 x 用于表示当前枚举深度,但是怎么判断当前可不可以呢?这里使用了全局变量。

有亿点细节就是全局变量的使用,由于dfs内核是递归,用一个全局变量可以记录每一次递归的状态。然后就是这个深度优先和模板有点不一样,因为这个起点并不是给定的。可以任意寻找每一个作业作为起点,于是在判断条件下面又加了一个dfs;

P1036 [NOIP2002 普及组] 选数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int n,k;
int a[30];
long long ans;
bool isPrime(int x){
	if(x==1) return false;
	for(int i=2;i*i<x;i++){
		if(x%i==0) return false;
	}
	return true;
}
void dfs(int m,int sum,int start){
	if(m==k){
		if(isPrime(sum)) ans++;
		return;
	}
	for(int i=start;i<=n;i++){
		dfs(m+1,sum+a[i],i+1);
	}
	return;
}
int main(){
	cin>>n>>k;	
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	sort(a+1,a+1+n);
	dfs(0,0,1);
	cout<<ans<<endl;
	return 0;
}

题意是n个数字,随机选取k个数字相加,问结果有多少个是质数。

首先考虑怎么传参,需要保存什么,肯定要保存当前存了几个数字,也要保存当前的和。但是为了不重复,在对数组排序后,还要保存起点,保证在用过 i 之后,下一个枚举的数字一定是i+1 。 

注意枚举之后要return,否则到后面一直凑不够k个数会导致程序没有出口。

Square - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int n,m,cnt,sum;
bool vis[101];
int a[101];
bool dfs(int stick,int len,int last){
	if(stick>3) return true;
	if(len==cnt) return dfs(stick+1,0,1);
	for(int i=last;i<=m;i++){
		if(!vis[i]&&a[i]+len<=cnt){
			vis[i]=1;
			if(dfs(stick,len+a[i],i)) return true;
			vis[i]=0;
		}
	}
	return false;
}
int main(){
	cin>>n;
	while(n--){
		sum=0;
		cin>>m;
		for(int i=1;i<=m;i++){
			cin>>a[i];
			sum+=a[i];
		}
		sort(a+1,a+1+m);
		reverse(a+1,a+1+m);
		if(sum%4!=0){
			cout<<"no"<<endl;
			continue;
		}
		cnt=sum/4;
		memset(vis,0,sizeof(vis));
		if(dfs(1,0,1)) cout<<"yes"<<endl;
		else cout<<"no"<<endl;
	}
}  

 题意是给一堆木棒,问可不可以拼成正方形。

考虑参数,首先需要考虑当前拼了多长,也需要考虑当前拼到了第几根,为了先用大的再用小的,保证大的拼完不会再使用,需要记录一个当前起点,使得选取这个木棒之后所有的木棒都比它短*(如果比它长的话可能没意义)。

这题的特点是dfs的返回值是bool值,这个函数每次return都只会return一个值,不能实现递归的效果,如果得出的结果是false,是需要继续运行的。所以bool值的bfs函数需要用if来控制出口。

二、广度优先搜索(BFS)

        给定一个起点,广度优先搜索会优先考虑与当前阶段最相近的状态,如果是迷宫,就会优先走上下左右四个方向。每个时刻要做的事情就是从上个时刻每个状态扩展出新的状态。借用队列实现。有的时候要存一个vis数组;把走过的路全标记上vis。

        模板如下:  

Q.push(初始状态);
while(!Q.empty()){
    state u=Q.front();//取出队首
    Q.pop();//出队
    for(枚举所有可扩展状态)//找到u的可达状态v
        if(是合法的)
            Q.push(v);
}

给几个例题:

P1443 马的遍历 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include <iostream>
#include <queue>
#include <cstring>
#include <iomanip>
using namespace std;
struct crood
{
	int x;
	int y;
};
int ans[410][410];
queue<crood> q1;
int main()
{
		int dis[8][2]={{1,2},{2,1},{-1,-2},{-2,-1},{1,-2},{-2,1},{-1,2},{2,-1}};
		crood pos;
		int m,n;cin>>m>>n;
		cin>>pos.x>>pos.y;
		memset(ans,-1,sizeof(ans));
		ans[pos.x][pos.y]=0;
		q1.push(pos);
		while(!q1.empty())
		{
			crood t=q1.front();
			q1.pop();
			for(int i=0;i<8;i++)
			{
				int ux=t.x+dis[i][0];
				int uy=t.y+dis[i][1];
				int d=ans[t.x][t.y];
				if(ans[ux][uy]!=-1||ux<1||ux>m||uy<1||uy>n)
					continue;
				ans[ux][uy]=d+1;
//				cout<<ux<<" "<<uy<<" "<<d+1<<endl;
				crood tmp={ux,uy};
				q1.push(tmp);
			}
		}
		
		for(int i=1;i<=m;i++)
		{
			for(int j=1;j<=n;j++)
			{
				cout<<setw(4)<<ans[i][j];
			}
			cout<<endl;
		}
		return 0;
				
		
}

可以用dis数组描述它的路径;走过的就vis一下,可以保证是最短。 

 P1135 奇怪的电梯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include <iostream>
#include <queue>
using namespace std;
struct node{
	int pos;
	int count;
};
int vis[300];
int num[300];
int pos1,count1;
int main()
{
	queue<node> q1; 
	int n,A,B;
	cin>>n>>A>>B;
	for(int i=1;i<=n;i++)
	{
		cin>>num[i];
	}
	node a;
	a.pos=A;vis[A]=1;
	q1.push(a);
	while(!q1.empty())
	{
		node t=q1.front();
		pos1=t.pos;
		count1=t.count;
		q1.pop();
		if(pos1==B) break;
		for(int i=-1;i<=1;i+=2)
		{
			int pos2=pos1+i*num[pos1];
			if(vis[pos2]==1||pos2<=0||pos2>n)
				continue;
			vis[pos2]=1;
			node tmp={pos2,count1+1};
			q1.push(tmp);
		}
	}
	if(pos1==B) cout<<count1<<endl;
	else cout<<-1<<endl;
	return 0;
	
}

一样,走过的vis一下,可以保证最短路径。 

 最重量级的来了:

Problem - 2612 (hdu.edu.cn)

#include <iostream> 
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
int n,m;
int a[300][300];
bool vis[300][300];
int kfc[300][300];
#define inf 0x3f3f3f3f
struct crood
{
	int x;
	int y;
	int step;
};
queue<crood> q1;
int dis[4][2]={{1,0},{-1,0},{0,1},{0,-1}};

crood Yi,Me;
void bfs(crood u)
{
	memset(vis,0,sizeof(vis));
	int x1,y1;
	q1.push(u);
	while(!q1.empty())
	{
		crood u1=q1.front();
		q1.pop();
		if(a[u1.x][u1.y]==-1)
		{
			kfc[u1.x][u1.y]+=u1.step;
			
		}
		
		for(int i=0;i<4;i++)
		{
			x1=u1.x+dis[i][0];
			y1=u1.y+dis[i][1]; 
			if(vis[x1][y1]==0&&x1>=1&&x1<=n&&y1>=1&&y1<=m&&a[x1][y1]!=-2)
			{
				crood tmp={x1,y1,u1.step+1};
				q1.push(tmp);
				vis[x1][y1]=1;
			}
		}
	}
}


int main()
{
	//构建图:
	char x;
	while(cin>>n>>m)
	{
		memset(kfc,0,sizeof(kfc));
			memset(a,0,sizeof(a));
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++)
			{
				cin>>x;
				if(x=='@') a[i][j]=-1,vis[i][j]=1;
				if(x=='#') a[i][j]=-2;
				if(x=='Y') Yi={i,j,0};
				if(x=='M') Me={i,j,0};
			}	
		}
		
		 bfs(Yi);
		 bfs(Me);
		 int min1=inf;
		 for(int i=1;i<=n;i++)
		 	for(int j=1;j<=m;j++)
		 		if(kfc[i][j]!=0)
		 		min1=min(min1,kfc[i][j]);
		 cout<<11*min1<<endl;
		}
	
	 return 0;
	 
}

在这里学到了一点,存图的技巧,可以直接存字符的图,障碍和判断走没走过的图是独立的。然后KFC也可以从字符的图里找,最后a+b就是每走到KFC就加当前的step,这样思路很清晰

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值