广度优先搜索——层层递进
广搜是最简便的图搜索算法之一,这一算法也是很多重要的图的算法的原型。
Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。
思想:一层一层往外扩散
广度优先搜索较之深度优先搜索之不同在于,深度优先搜索旨在不管有多少条岔路,先一条路走到底,不成功就返回上一个路口然后就选择下一条岔路。而广度优先搜索旨在面临一个路口时,把所有的岔路口都记下来,然后选择其中一个进入,然后将它的分路情况记录下来,然后再返回来进入另外一个岔路。并重复这样的操作。
来个简单的例子模拟BFS的过程:
(首先,我们默认优先选择靠左或靠下的元素进行访问 )
选择A为源节点---->(继续往下一层搜索)A的子节点有两个,所以搜索到了B和C两个节点---->(继续下一层搜索,按照靠下和靠左优先原则)先搜索B的所有子节点D---->(C的子节点还没有搜素,所以再要搜索C的子节点)C的子节点已经访问了,所以到了下一层搜素,也就是开始搜素D的子节点E---->结束。
因此此图的遍历顺序为:A---->B---->C---->D---->E(注意对比DFS)
这个例子如果不好掌握的话,我们再看一个非常常见的例子吧:
就像我们把一个石子扔进池塘产生的波纹一样,用像波纹这样一层层往外扩散来遍历整个池塘的方法就是BFS的样子。
当然,我们程序中无法在同一时刻向外访问一圈,所以我们采取队列的模式来模拟。(可以看下文具体演示)
队列模拟同时向外的访问如下:
首先我们选源节点入队,访问源节点的每一个next邻节点(也就是让这些节点都入队),源节点的所有next节点都访问完之后,源节点出队,开始访问此时队首的每一个next邻节点(也就是让这些节点都入队)。做完当前队首的所有next邻节点入队之后。对首出队,开始访问此时队首的每一个next邻节点。重复上述过程,直到所有的点都被访问完毕(也就是队列里的数据都出队了)。
总结广搜的思路:像石子激起水花一样,一层层的往外扩散式遍历。(深搜是深入式遍历)
我们拿迷宫的例子直接体会BFS:
迷宫有n行m列的单元格组成,每个单元格要么是空地,要么是障碍物,找到一条从起点到终点的最短路径。
输入迷宫地图和始末点坐标。
比如说是这样的图:
菱形为起始点,心形为终点,六角星为障碍物。我们的解决方法便是,从起始点开始,往下广搜,每次往进搜索一步便让步数+1,搜索到心形时记录下步数,搜索完毕。
我们来模拟一下这个过程:
从(1,1)出发,下一步可以走到(1,2),(2,1)点:
下一步我们要访问上一步走到的两个点的所有子节点(当然,有些点是障碍不能访问):
同理,我们可以得到所有的步骤图:
我们搜到了心形,也就是找到了目标物,我们每一步都会计数,到现在,我们广搜了几层,就是我们当前走到心形的步数。我们通过广搜解决了问题。
重点来了!!!代码实现这个算法思路:
仔细感悟刚刚的步骤,我们用一个队列来模拟这个过程。这里我们还是用一个结构体来实现队列。
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;
然后从(1.1)开始走,访问每一个next邻节点(逐一入队):
对(1,1)扩展完毕之后,那么现在(1,1)对我们已经没有用了,我们让(1,1)出队,很简单只需要:
head++;
接下来我们还要拓展刚刚新拓展出的(1,2)和(2,1)。现在head指向了(1,2),那么我们就拓展head:我们发现(1,2)没有可扩展的子节点,那么我们继续head++,扩展现在的head(也就是(2,1)):
我们不断的拓展(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语言