欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

广搜(BFS)

程序员文章站 2022-06-11 21:35:05
...

广度优先搜索——层层递进

广搜是最简便的图搜索算法之一,这一算法也是很多重要的图的算法的原型。

Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。

思想:一层一层往外扩散

广度优先搜索较之深度优先搜索之不同在于,深度优先搜索旨在不管有多少条岔路,先一条路走到底,不成功就返回上一个路口然后就选择下一条岔路。而广度优先搜索旨在面临一个路口时,把所有的岔路口都记下来,然后选择其中一个进入,然后将它的分路情况记录下来,然后再返回来进入另外一个岔路。并重复这样的操作。

来个简单的例子模拟BFS的过程:
广搜(BFS)
(首先,我们默认优先选择靠左或靠下的元素进行访问 )
选择A为源节点---->(继续往下一层搜索)A的子节点有两个,所以搜索到了B和C两个节点---->(继续下一层搜索,按照靠下和靠左优先原则)先搜索B的所有子节点D---->(C的子节点还没有搜素,所以再要搜索C的子节点)C的子节点已经访问了,所以到了下一层搜素,也就是开始搜素D的子节点E---->结束。
因此此图的遍历顺序为:A---->B---->C---->D---->E(注意对比DFS)
这个例子如果不好掌握的话,我们再看一个非常常见的例子吧:
广搜(BFS)
就像我们把一个石子扔进池塘产生的波纹一样,用像波纹这样一层层往外扩散来遍历整个池塘的方法就是BFS的样子。
当然,我们程序中无法在同一时刻向外访问一圈,所以我们采取队列的模式来模拟。(可以看下文具体演示)
队列模拟同时向外的访问如下:

首先我们选源节点入队,访问源节点的每一个next邻节点(也就是让这些节点都入队),源节点的所有next节点都访问完之后,源节点出队,开始访问此时队首的每一个next邻节点(也就是让这些节点都入队)。做完当前队首的所有next邻节点入队之后。对首出队,开始访问此时队首的每一个next邻节点。重复上述过程,直到所有的点都被访问完毕(也就是队列里的数据都出队了)。

总结广搜的思路:像石子激起水花一样,一层层的往外扩散式遍历。(深搜是深入式遍历)

我们拿迷宫的例子直接体会BFS:
迷宫有n行m列的单元格组成,每个单元格要么是空地,要么是障碍物,找到一条从起点到终点的最短路径。
输入迷宫地图和始末点坐标。
比如说是这样的图:
广搜(BFS)
菱形为起始点,心形为终点,六角星为障碍物。我们的解决方法便是,从起始点开始,往下广搜,每次往进搜索一步便让步数+1,搜索到心形时记录下步数,搜索完毕。
我们来模拟一下这个过程:
从(1,1)出发,下一步可以走到(1,2),(2,1)点:
广搜(BFS)
下一步我们要访问上一步走到的两个点的所有子节点(当然,有些点是障碍不能访问):
广搜(BFS)
同理,我们可以得到所有的步骤图:
广搜(BFS)
广搜(BFS)
广搜(BFS)
广搜(BFS)

我们搜到了心形,也就是找到了目标物,我们每一步都会计数,到现在,我们广搜了几层,就是我们当前走到心形的步数。我们通过广搜解决了问题。

重点来了!!!代码实现这个算法思路:

仔细感悟刚刚的步骤,我们用一个队列来模拟这个过程。这里我们还是用一个结构体来实现队列。

struct node {
	int x;//行数 
	int y;//列数 
	int s;//步数 
};
int map[55][55];//地图 
struct node que[2510];//定义了一个队列数组 
int head,tail;//队列头尾
int book[55][55]={0};//标记数组,用来标记已经遍历的点,这可不能往回搜索
//队列初始化,即队列置空 
head=1;
tail=1;
//第一步将(1,1)入队,并标记
que[tail].x=1;
que[tail].y=1;
que[tail].s=0;
tail++;
book[1][1]=1; 

广搜(BFS)
然后从(1.1)开始走,访问每一个next邻节点(逐一入队):
广搜(BFS)
对(1,1)扩展完毕之后,那么现在(1,1)对我们已经没有用了,我们让(1,1)出队,很简单只需要:
head++;
接下来我们还要拓展刚刚新拓展出的(1,2)和(2,1)。现在head指向了(1,2),那么我们就拓展head:我们发现(1,2)没有可扩展的子节点,那么我们继续head++,扩展现在的head(也就是(2,1)):
广搜(BFS)
我们不断的拓展(step便是head的步数+1,因为它是head拓展出来的节点),拓展完head的步数,便head出队。如此往下进行,直到找到目标为止。
我们为了方便访问拓展head的子节点,我们定义了一个含有四个方向的next数组。

int next[4][2]={{0,1},//向右 
			{1,0},//向下 
			{0,-1},//向左 
			{-1,0}};//向上 

我们看看完整代码:

#include <stdio.h>

struct node {
	int x;//行数 
	int y;//列数 
	int s;//步数 
};
int map[55][55];//地图 
struct node que[2510];//定义了一个队列数组 
int head,tail;//队列头尾
int book[55][55]={0};//标记数组,用来标记已经遍历的点,这可不能往回搜索
int n,m;//地图大小
int p,q;//目标坐标 

 
void BFS(int sx,int sy)//起始的坐标
{
	int i,tx,ty,flag;//下一点坐标,flag表示有无找到目标 
	//定义的方向数组 
	int next[4][2]={{0,1},//向右 
					{1,0},//向下 
					{0,-1},//向左 
					{-1,0}};//向上

	while(head<tail){//队列不为空的时候,循环 
		for(i=0;i<4;i++){//枚举四个方向 
			//计算下一点坐标 
			tx=que[head].x+next[i][0];
			ty=que[head].y+next[i][1];
			//判断是否越界
			if(tx<1||ty<1||tx>n||ty>m){
				continue;//越界便continue下一方向 
			}
			//判断是否是障碍或者已经在路径中了
			if(map[tx][ty]==0&&book[tx][ty]==0){
				//标记这个点,注意,此时不需要取消标记,因为这个点只需走一次,和深搜不同
				book[tx][ty]=1;
				//入队
				que[tail].x=tx;
				que[tail].y=ty;
				que[tail].s=que[head].s+1;
				tail++; 
			}
			if(tx==p&&ty==q){//到目标了,结束 
				flag=1;
				break; 
			} 
		}
		if(flag==1){
			break; 
		} 
		head++;//已经拓展过子节点的点,出队 
	}
	return ;
}
int main()
{
	int i,j; 
	int sx,sy; 
	scanf("%d %d",&n,&m); //读入地图大小
	for(i=1;i<=n;i++){
		for(j=1;j<=m;j++){
			scanf("%d",&map[i][j]);
		}
	}
	scanf("%d %d %d %d",&sx,&sy,&p,&q);//起始点和目标 
	//队列初始化,即队列置空 
	head=1;
	tail=1;
	//第一步将起始点入队,并标记
		que[tail].x=sx;
		que[tail].y=sy;
		que[tail].s=0;
		tail++;
		book[sx][sy]=1; 
	BFS(sx,sy);
	//打印最后一个点(目标点)的步数 
	printf("%d",que[tail-1].s); 
	return 0;
}

思考:这个代码中是否存在一些不可见的bug呢?
答案:如果目标点不可被找到呢?------这就需要聪明的你进行探索了!

好了,如果还想进一步学习BFS的话,不如看完关于BFS实践的一篇题解吧:HDU-2612-Find a way(BFS)题解-C语言