初步学习BFS的心得体会

BFS问题初探

      BFS,BFS,其英文全称是Breadth First Search,指广度优先搜索.该类问题利用了STL容器queue--队列,进行搜索.原理是在保证当前状态下,将此状态入队列,取出此状态,队列消除该状态,再将该状态往下一步发展的所有可能入队列,再将每种可能取出,并且将着它所有可能发展的方向入队,记录步数,直到得到目的.

要想学会bfs,必须先了解队列.我将bfs中可能涉及的队列的函数列举出来.

1.创建队列对象:queue<队列中元素类型>队列名(队列名自己取);(ps:对于元素类型,除了int,char以外,自己定义的类型也可以)

2.队尾添加元素:队列名.push(元素名);

3.队首去除元素:队列名.pop();

4.访问队首:队列名.front();

5.访问队尾:队列名.back();

6.判断队列是否为空:队列名.empty();

7.返回队列大小:队列名.size();

好了,工欲善其事必先利其器,器已在手,我们就可以初步去了解bfs.

下面是第一个例题.


逃亡的奶牛:农夫约翰已被告知一头逃亡的奶牛的位置,并希望立即将其抓住。 他从数字线上的点N(0≤N≤100,000)开始,而母牛在同一数字线上的点K(0≤K≤100,000)开始。 农夫约翰有两种运输方式:步行和传送。行走:约翰可以在一分钟内从任意点X移至点X-1或X + 1.传送:FJ可以在一分钟内从任意X点移动到2×X点。如果没有意识到他的追捕能力的母牛完全没有动弹,那么农夫约翰要花多长时间才能找回它?

输入

第1行:两个以空格分隔的整数:N和K

输出

第1行:农夫约翰用最少的时间(以分钟为单位)捉住逃犯。

样本输入

5 17

样本输出

4

ps:农夫约翰到达逃犯的最快方法是沿着以下路线移动:5-10-9-18-17,这需要4分钟。


我先把我的代码放出来,再进行讲解:

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
int vis[100007];
struct john{
	int now,step;//记录John的位置,步数
};
void bfs(int x,int y){
    john bef,nex;
	queue<john>qu;
	bef.now=x;
	bef.step=0;
	vis[bef.now]=1;//记录初始状态
	qu.push(bef);
	while(qu.empty()!=1){
		bef=qu.front();
		qu.pop();
		if(bef.now==y){
			cout<<bef.step;
			return;
		}//退出条件
		//-------------向前一步 
		if(vis[bef.now+1]==0&&((bef.now+1)<=100000)){
			nex.now=bef.now+1;
			nex.step=bef.step+1;
			vis[nex.now]=1;
			qu.push(nex);
		}
		//-------------向后一步
		if(vis[bef.now-1]==0&&((bef.now-1)>=0)){
			nex.now=bef.now-1;
			nex.step=bef.step+1;
			vis[nex.now]=1;
			qu.push(nex);
		}
		//-------------特殊
		if(vis[2*bef.now]==0&&((bef.now*2)>=0)){
			nex.now=bef.now*2;
			nex.step=bef.step+1;
			vis[nex.now]=1;
			qu.push(nex);
		} 
	}
	cout<<"-1";
}
int main(){
	int john,cow;
	memset(vis,0,sizeof(vis));
	cin>>john>>cow;
	bfs(john,cow);
}

然后是问题分析:

入题目所示,农夫有两种运动方式,最多有三种运动所的结果,为什么是最多呢,因为当约翰在0或者N位置的时候,分别不能往后,往前走,这也是要考虑的条件之一.理解题目所给的追捕方式,无非就是往前一步,往后一步,瞬移到两倍下标的位置(例如从3直接到6,从2直接到4).了解此问题后,我们直接敲代码.

首先就是初始处理,关于如何储存农夫此时的状态和步数,struct结构体似乎是一个不错的选择:

struct john{
    int now;//记录此时状态,在某个点
    int step;//记录从初始位置John走了多少步
};

当然,还有一个问题我没有提及,就是对于bfs重复的处理,如果john没睡醒,两次到达了点5,那是不是其实第二次是完全没有必要的呢?对,进行第二次的话毫无意义,因为我已经到过该点,后续操作将与第一次完全重合,浪费空间时间,此时我们可以引入一个标记数组.

int vis[100007];

标记数组的大小依照题目来看,类型也可以是bool型,只要能达到它的作用,标记john已经到达的位置即可,我们可以设john没到过的地方为0,当要进行下一步操作时,我们可以判断,如果vis[下一步到达的地点下标]==0,那么就可以到达此处,并且将该种情况入队记录后,将vis[该点下标]更新为1,那么下次到达该点也就不会进行入队操作.

然后就是进行对john初始状态的处理,直接用一个结构体将其初始状态记录:

    john bef;//此处定义同类型结构体
    queue<john>qu;//创建队列
    bef.n=x;//将john一开始的位置记录在结构体里面
    bef.step=0;//此时john还没有动
    vis[bef.n]=1;//记录john已经到过该点

记录完初始状态,就把他入队.在进入一个while循环.该循环为while(qu.empty()!=1).进入这样一个循环是为什么呢?因为我们在此循环中会将每一种可能进行搜索,如果不符合情况就不会入队,循环往复,当搜索多次遇见不能够入队的情况,可能是所有的下一步都已经被标记,而qu队列又需要出队元素,当队列为空时,也就说明没有往下一步的可能了,但是仍然没有达到目标地,这就是无法到达的情况,当while循环到队列了,还未到达,可以按题意输出NO了.

然后就是退出条件,也就是满足了题目条件,我们可以取出队首元素直接与目的地对比,如果相等直接输出步数,return.

if(bef.now==y){
	cout<<bef.step;
	return;
	}//退出条件

最后需要讲解的就是对于每次到达一个点后,他下一步可能到达的地点.

                //向前
                if(vis[bef.now+1]==0&&((bef.now+1)<=100000)){
			nex.now=bef.now+1;
			nex.step=bef.step+1;
			vis[nex.now]=1;
			qu.push(nex);
		}
		//-------------向后一步
		if(vis[bef.now-1]==0&&((bef.now-1)>=0)){
			nex.now=bef.now-1;
			nex.step=bef.step+1;
			vis[nex.now]=1;
			qu.push(nex);
		}
		//-------------特殊
		if(vis[2*bef.now]==0&&((bef.now*2)>=0)){
			nex.now=bef.now*2;
			nex.step=bef.step+1;
			vis[nex.now]=1;
			qu.push(nex);
		} 

可以看到,每次我都进行了vis的判断,如果没到过,并且没有到边界,我就会进行入队操作,这里的到边界,以后很多题会出现,而且更加复杂,这里仅仅要注意在0位置不能往前走,在
N位置不能往后走,这种情况也要重点注意哦.对于后续可能情况处理,我引入了新的结构体变量,nex,他的好处就是我可以保留上一步,也就是bef的状态,将下一步进行操作(位置的改变,步数的增加,标记点).

这是一个挺简单的bfs了,bfs的模板基本如此,记录初始状态,while循环,,循环中退出条件,不能退出就往下一步搜索.其实bfs难点还是在于边界判断,标记,往下一步搜索下一步所有的可能性.

下面我再举两个题,针对它们的标记和退出条件讲解.(毕竟bfs基础题的话样子都差不多,不同的就是1退出条件和下一步分支,标记等).


问题描述

有一个奇怪的升降机,升降机可以根据需要停在每个楼层,每个楼层上都有一个数字Ki(0 <= Ki <= N),该升降机只有两个按钮:上和下。 在第i层,如果您按“ UP”按钮,您将在Ki层上,即,您将进入第i + Ki层,同样,如果您按“ DOWN”按钮,则将在下层 Ki楼层,即您将进入i-Ki楼层。 当然,电梯不能高于N,不能低于1。例如,有一个5层楼的建筑物,k1 = 3,k2 = 3,k3 = 1,k4 = 2,k5 =5。从1楼开始,您可以按“ UP”按钮,然后您将上升到4楼,如果您按“ DOWN”按钮,则电梯不能 它,因为它不能下降到-2楼,如您所知,-2楼不存在。

问题来了:当您在A楼上并且想去B楼时,他至少必须按“上”或“下”按钮多少次?

输入

输入包含多个测试用例。每个测试用例包含两行。

第一行包含上述三个整数N,A,B(1 <= N,A,B <= 200),第二行包含N个整数k1,k2,.... kn。

单个0表示输入的结尾。

输出

对于每种输入输出情况,整数时,在A楼时必须最少按下一次按钮,然后才想转到B楼。如果无法到达B楼,则打印“ -1”。

样本输入

5 1 5

3 3 1 2 5

0

样本输出

3


本题也是基础的bfs,可以把上题搞明白后依葫芦画瓢,套用模板,注意退出条件,分支可能,标记即可.先上代码:

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
int flo[201];//记录该层可以操作的层数
int vis[201];//标记是否已经到达
struct floor{
	int now;//现在所处状态
	int step;//步数
};
void bfs(int N,int A,int B){
	queue<floor>qu;
	floor bef,nex;
	bef.now=A;
	bef.step=0;
	vis[A]==1;//记录初始状态
	qu.push(bef);
	while(qu.empty()==0){
		bef=qu.front();
		qu.pop();
		if(bef.now==B){
			printf("%d\n",bef.step);
			return;
		}//退出条件
		else{
			nex.now=bef.now+flo[bef.now];
			if(nex.now<=N&&vis[nex.now]!=1){
				nex.step=bef.step+1;
				vis[nex.now]=1;
				qu.push(nex);
			}//往上走
			nex.now=bef.now-flo[bef.now];
			if(nex.now>=1&&vis[nex.now]!=1){
				nex.step=bef.step+1;
				vis[nex.now]=1;
				qu.push(nex);
			}//往下走
		}
	}
	printf("-1\n");//结果不存在
	return;
}
int main(){
	int N,A,B;//N为共有多少层,A起始,B结尾
	while(scanf("%d",&N)&&N!=0){
	scanf("%d%d",&A,&B);
	memset(vis,0,sizeof(vis));//初始化标记数组
	for(int i=1;i<=N;i++){
		scanf("%d",&flo[i]);//记录每一层可以上下的层数
	}
	bfs(N,A,B);
	}
}

该题有一个数组,用来存楼层上可操作数,我之前提到过可以套模板,但是切忌生搬硬套,仔细审题后,将题目任务拆分逐步实现,不能漏下一个,这也是我写代码的思想.

针对该题与上题有一个点要注意一下,多组数据的读入,如果粗心会发生一个问题,忘记将vis数组初始化而对下一组数据进行搜索,这样很可能导致可以到达目的地的代码无法搜索到最后,因为你上一组的标记没删除,令这组本该到达的地方没有到达,一定要注意,细节决定成败,bfs代码初学可能较长,不注重细节几百行debug到头颅爆炸.

memset(vis,0,sizeof(vis));//初始化标记数组
//每次循环都要初始化

注意本题也有边界,需要判断下一步到达的楼层到底存不存在.

然后就是最后一个题:


Problem Description

大家一定觉的运动以后喝可乐是一件很惬意的事情,但是seeyou却不这么认为。因为每次当seeyou买了可乐以后,阿牛就要求和seeyou一起分享这一瓶可乐,而且一定要喝的和seeyou一样多。但seeyou的手中只有两个杯子,它们的容量分别是N 毫升和M 毫升 可乐的体积为S (S<101)毫升 (正好装满一瓶) ,它们三个之间可以相互倒可乐 (都是没有刻度的,且 S==N+M,101>S>0,N>0,M>0) 。聪明的ACMER你们说他们能平分吗?如果能请输出倒可乐的最少的次数,如果不能输出"NO"。

Input

三个整数 : S 可乐的体积 , N 和 M是两个杯子的容量,以"0 0 0"结束。

Output

如果能平分的话请输出最少要倒的次数,否则输出"NO"。

Sample Input

7 4 3

4 1 3

0 0 0

Sample Output

NO

3


该题比较麻烦的就是每次倒可乐后下一种可能的分支,可能有六种,s->n,s->m,n->s,n->m,m->s,m->n.每一种情况都不能漏,目前有两个思路,1.直接将每种情况枚举,2.建立两个小数组,分别将每次的s,m,n中可乐可能情况存入,再使用for循环倒水(此代码我的学长已经实现,我还没写出来).我采用的第一种方法.第二个难点就是标记,三个容器如何标记?因为本题数据不算太大,我采用了三维数组vis[101][101][101],数组长度没有超过1e8.代码呈现如下:

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
int vis[101][101][101];//标记状态
struct kola{
	int s;
	int n;
	int m;//s,n,m中可乐量
	int step;//步数
};
bool che(kola bef){
	if((bef.m==bef.n)&&bef.s==0)return false;
	else if((bef.m==bef.s)&&bef.n==0)return false;
	else if((bef.s==bef.n)&&bef.m==0)return false;
	else return true;
}//退出条件封装函数,当三者之中一个空,两个相等,即可停止搜索.
void bfs(int S,int N,int M){
	int a,b,c;
	memset(vis,0,sizeof(vis));
	queue<kola>qu;
	kola bef,nex;
	bef.s=S;
	bef.m=0;
	bef.n=0;
	bef.step=0;
	vis[bef.s][bef.n][bef.m]=1;//记录初始状态
	qu.push(bef);
	while(qu.empty()!=1){
		bef=qu.front();
		qu.pop();
		a=bef.s;
		b=bef.n;
		c=bef.m;//abc比较短,方便记录,平时不建议写这种变量名,变量名一多容易搞混
		if(che(bef)==false){
			printf("%d\n",bef.step);
			return;
		}
	//--从S倒---------------------------------------------//
		if(N-b>=a&&vis[0][a+b][c]==0){
			qu.push({0,a+b,c,bef.step+1});
			vis[0][a+b][c]=1;
		}                                     //可以将s全部倒进n
		
                else if(N-b<a&&vis[a-(N-b)][N][c]==0){
			qu.push({a-(N-b),N,c,bef.step+1});
			vis[a-(N-b)][N][c]=1;        //不可以将s全部倒进n

		}//-------------------------------s向n倒水
		if(M-c>=a&&vis[0][b][a+c]==0){
			qu.push({0,b,a+c,bef.step+1});
			vis[0][b][a+c]=1;
		}                                     //可以将s全部倒进m

		else if(M-c<a&&vis[a-(M-c)][b][M]==0){
			qu.push({a-(M-c),b,M,bef.step+1});
			vis[a-(M-c)][b][M]=1;         //不可以将s全部倒进n(ps:下面两段与上面几乎相同)

		}//--------------------------------s向m倒水
        //--从N倒----------------------------------------------//
		if(S-a>=b&&vis[a+b][0][c]==0){
			qu.push({a+b,0,c,bef.step+1});
			vis[a+b][0][c]=1;
		}
		else if(S-a<b&&vis[S][b-(S-a)][c]==0){
			qu.push({S,b-(S-a),c,bef.step+1});
			vis[S][b-(S-a)][c]=1;
		}//--------------------------------n向s倒水
		if(M-c>=b&&vis[a][0][b+c]==0){
			qu.push({a,0,b+c,bef.step+1});
			vis[a][0][b+c]=1;
		}
		else if(M-c<b&&vis[a][b-(M-c)][M]==0){
			qu.push({a,b-(M-c),M,bef.step+1});
			vis[a][b-(M-c)][M]=1;
		}//--------------------------------n向m倒水
	//--从M倒---------------------------------------------//
		if(S-a>=c&&vis[a+c][b][0]==0){
			qu.push({a+c,b,0,bef.step+1});
			vis[a+c][b][0]=1;
		}
		else if(S-a<c&&vis[S][b][c-(S-a)]==0){
			qu.push({S,b,c-(S-a),bef.step+1});
			vis[S][b][c-(S-a)]=1;
		}//---------------------------------m向s倒水
		if(N-b>=c&&vis[a][b+c][0]==0){
			qu.push({a,b+c,0,bef.step+1});
			vis[a][b+c][0]=1;
		}
		else if(N-b<c&&vis[a][N][c-(N-b)]==0){
			qu.push({a,N,c-(N-b),bef.step+1});
			vis[a][N][c-(N-b)]=1;
		}//--------------------------------m向n倒水
		
//-------------------------------------------------//
		
	}
	printf("NO\n");
	return;//没有可能相等
}
int main(){
	int S,N,M;
	while(scanf("%d%d%d",&S,&N,&M)&&N!=0){
	bfs(S,N,M);
	}
}

当把s向n,m倒可乐写出来了,可以直接复制粘贴然后改数据写出所有情况.(类比)

 

 

 

bfs比较注重于考验细心(个人观点),你要考虑到是否有边界,如何标记,一共有多少种情况......当然,在完成基本代码后可以进行优化,就像刚才倒可乐的题目,第二种思想大家也可以思考一下.

我也是个初学者,这是第一次写博客,下次会尝试写一下dfs,有什么不足希望大家指出来.qwq

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值