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

广度优先搜索(BFS)

程序员文章站 2022-06-11 20:37:07
...

广度优先搜索(BFS)

广度优先搜索,英文名字为Breadth-First Search,又被称为是宽度优先搜索。它与深度优先搜索相同的是,它也是从某一个状态出发可以到达所有的状态。

也深度优先搜索不同的是,它们的搜索顺序存在差异。广度优先搜索,顾名思义,它以广度为优先搜索的对象。它总是去搜索距离初始状态近的状态(而不是像深度优先搜索那样一路走到黑,再退回来)。换句话说,它是按照开始状态——>只需要一次转移就可以到达的状态——>只需要两次就可以到达的状态——>……这样的顺序进行搜索。对于同一种状态,宽度优先搜索只经过一次,因此复杂度为 O(×)O(状态数 \times 转移的方式)
广度优先搜索(BFS)


队列

首先我们先要介绍一下队列的有关知识,来继续我们的学习。

关于队列是什么,可以看这个链接

戳这里

然后我们还需要知道如何实现一个队列。在这里提供两种方法。一种用数组模拟,一种用C++std中的queue。

数组模拟数列:

#include<stdio.h>
int L,R;
int que[N];//用来储存数据的一个队列
/*
要注意,只有在[L,R]之间的数字,才是队列中的元素,其中L是队首,R是队尾。
*/
void push(int x){//在队列中加入一个元素x(加在队尾)	
  	que[++R]=x;
  	//R++;que[R]=x;
}
void pop(){//删除队首的元素
  	L++;
}
int front(){//访问队列首的元素
  	return que[L];
}
bool empty(){//判断数列是否为空
  	return (R<L);
}
int main(){
    L=0,R=-1;//此时队列中无元素
 	push(1);//1
  	push(2);//1 2
  	push(3);//1 2 3
  	pop();//2 3
  	push(4);//2 3 4
  	pop();//3 4
  	int ans=front();//ans=3;
  	return 0;
}

//如上所示即可实现一个简单的队列

C++ STD中的queue:

#include<queue>
#include<iostream>
using namespace std;

queue<int>que;//声明一个int类型的队列,名字叫做que

int main(){
  	que.push(1);//1
  	que.push(2);//1 2
  	que.push(3);//1 2 3
  	que.pop();//2 3
  	que.push(4);//2 3 4
  	pop();//3 4
  	int ans=que.front();//ans=3
  	return 0;
}

还有链表实现等方法,有兴趣的同学可以上百度搜一下。


广搜的在竞赛中的应用

一般来说,广搜的性质,决定了,它适用于那种求最短路或者最少操作数等的问题,适用于状态数量少,并且可以有效判重的问题。在广搜的问题上,一种状态只会被访问一次。为了避免状态的重复访问,所以我们需要判重。


广搜的现实

简单来说

深度优先(隐式地)利用了栈进行计算,而宽度优先搜索则利用了队列。

搜索时先将初始状态加入到队列中,此后从队列的最前端不断取出状态,把从该状态可以转移到的状态中尚未访问过的部分加入队列,如此往复,直到队列被取空或者已经找到了问题的解。通过观察这个队列我们就知道所有状态都是按照距离初始状态由近及远的顺序遍历的。


状态

广搜很重要的一个东西就是状态的把握(其实不只是广搜,深搜也一样)。

拿迷宫问题来举例。

如果我们要求的是最短路径的长度。那么在我们的状态中,就需要记录每一个状态第一次被访问到的时候的已经走过的步数。(由广搜由近及远的搜索顺序,我们不难证明,这个步数就是的最短路)

如果还需要求最终的路径。那么在我们的状态中,还需要记录,每一个状态第一次被访问到的时候,是由哪一个 状态而来的,也就是说把上一个状态也记录下来。这样最终就可以回溯上去,找到路径。

如果这个迷宫还有门、钥匙一类的设定。那么我们的状态中,就需要在记当前状态是不是有钥匙,是什么类型的钥匙等等。

但不论题目怎么变,广搜最终的实现是没有变的。都由队列来实现。


状态的判重

广搜很重要的一点就是判重。因为我们只允许一个状态走一遍。

拿迷宫问题来举例。

我们走到了(1,1)这个点以后,可以往(1,2)走,而(1,2)又可以走回到(1,1)。

但此时到达(1,1)一定不是最短距离了。

记住一句话,由广搜的性质,我们不难看出:第一次到达一个状态的时候,就是起始状态到这个状态的最短路程了,以后到达这个状态,不会比这次更短。

因此需要判重。对于一个已经走过的状态,我们就没有必要继续走了,因为已经有更优的解。

这样保证了每个状态只走一遍,也保证了算法的复杂度足够优秀。


一道例题:

Description:

给定一个N*M的迷宫。迷宫由通道和墙壁组成,可以向上下左右四个方向移动。求起点到终点所需要的最小步数。

样例:

Input:

N=10,M=10 (”S“为起点,”G“为终点,”#“为墙壁,“.”为通道)

广度优先搜索(BFS)

output:

22


Solution:

这个问题中,状态仅仅是目前所在的位置以及当前所走的步数。所以我们可以使用一个struct把状态封装起来。

struct node{
  	int x,y,step;
};
//node是节点的意思
//在这里用一个结构体把状态封装起来了
//一个状态有三个量 分别是横坐标x 纵坐标y 当前步数step

关于结构体(有同学反映你们没有学过)

简单介绍一下结构体:


//	简单介绍一下结构体:
//	结构体封装成的数据类型和我们平时见到的数据类型声明的方法一致
	node cur;//意思是声明了一个变量cur为node类型
//当然类型的名字和里面包含的变量可以随便你们自己取
struct AAA{
  	int a;char c;
};
	AAA num;//意思是声明了一个变量num为AAA类型

//而我们通常使用"."来访问类型中的量,比如:cur.x
//或者num.c
//(cur.x是int类型,num.c是char类型)

回到题目本身来:

那么我们可以用下面的代码来实现:

#include<stdio.h>
const int N=105;
char map[N][N];//存所给的地图
int n,m;
struct node{//状态有三个
  	int x,y,step;//横坐标 纵坐标 步数
}que[N*N];//用来充当队列的数组

bool mark[N][N];//用来判重的数组,如果(x,y)到达过,那么mark[x][y]=true

int dx[]={0,0,1,-1};
int dy[]={1,-1,0,0};//分别朝着四个方向走

bool judge(int x,int y){//判断(x,y)在不在地图里
  	if(x>0&&y>0&&x<=n&&y<=m)return true;
  	return false;
}

void BFS(int sx,int sy){//以(sx,sy)为起点开始进行广搜
  	int L=0,R=-1;//队列的左右标 R<L 此时队列中没有元素
  	node cur=(node){sx,sy,0};//定义一个cur  cur.x=sx cur.y=sy cur.step=0 (也就是初始状态)
  	que[++R]=cur;//把初始状态放入队列中
  	mark[0][0]=true;//标记这个状态已经走过了
  	while(L<=R){//如果队列里面还有元素
      	node now=que[L++];//从队首取出一个元素
      	for(int i=0;i<4;i++){//分别朝上下左右四个方向走
         	node nxt;//下一个状态
          	//分别计算下一个状态的横坐标 纵坐标 和步数
          	nxt.x=now.x+dx[i];
          	nxt.y=now.y+dy[i];
          	nxt.step=now.step+1;

          
          	if(map[nxt.x][nxt.y]=='G'){//如果已经找到了终点 直接输出
              	printf("%d\n",nxt.step);
              	return;
          	}
          	//如果下一个状态可以转移
          	//也就是说 下一个状态 在地图中 并且不是墙壁 并且以前没有走过
          	if(judge(nxt.x,nxt.y)&&!mark[nxt.x][nxt.y]&&map[nxt.x][nxt.y]!='#'){
              	que[++R]=nxt;//那么我们把转移后的状态加入队列尾部
              	mark[nxt.x][nxt.y]=true;//不能忘记标记掉 以免下次重复走
          	}
      	}
  	}  
}

int main(){
  memset(mark,0,sizeof(mark));	//清空mark数组 意思是 把mark初始值全部设为0
  scanf("%d%d",&n,&m);
  	for(int i=1;i<=n;i++)
      	scanf("%s",map[i]+1);
  	for(int i=1;i<=n;i++){
      	for(int j=1;j<=m;j++){
         	if(map[i][j]=='S')BFS(i,j); //找到起点并开始BFS
      	}
  	}
  	return 0;
}

上面的代码大致能够体现BFS的过程,希望大家仔细阅读注释。


广搜和深搜的比较

广搜和深搜一样都可以遍历所有状态,因此需要对所有状态进行处理时,用两者都可以。但是递归函数可以很简短地编写,而且状态的管理可以更加简单,所以大多数时候可以用深搜来写。反之,在求取最短路的时候,深度优先搜索需要反复经过同样的状态,深搜的效率就会很低,地图大的话根本跑不出来,所以还是使用广搜比较好。

从空间角度上来说,广度优先搜索要把所有状态都加到队列,所以通常需要与状态数成正比地内存空间。反之,深度优先搜索空间与最大递归深度成正比。一般状态数相比,递归深度不会太大,所以可以认为深度优先搜索更加节省内存。