一篇文章完全弄懂广度优先搜索(BFS)

上文复习了深度优先搜索,我们这篇文章来看看广度优先搜索(BFS)。不要着急,我们先复习一下队列。


队列(Queue)

在讲广度优先搜索时,我们先来复习一下队列(Queue),这里不把队列作为介绍的重点,基本功扎实的小伙伴可以跳过这一部分。

队列是一种数据结构,也就是存放、操作数据的方式,重点需要掌握的是他的逻辑——“先进先出”。跟我们生活中排队上车一样,先进入队列的,先刷卡上车,后进入队列的人只能等前面的人离开队列,才能向前移动。

队列的主要操作

  1. 入队(push)
  2. 出队(pop)
  3. 判断队列是否为空(empty)
  4. 统计队列元素个数(size)
  5. 访问队首元素(front)

在C++标准模板库中,我们已经有队列实现好了的模板。

#include<queue> //queue头文件
queue<T> q; //构建一个T类型的队列
q.push(XX); //入队
q.pop(); //出队
q.front() //获得队首元素
q.empty(); //判断队列是否为空,在pop之前需要检查一下

队列的介绍到此结束,如果有点懵的话,建议先好好学习一下队列,再继续学习广度优先搜索,因为广度优先搜索可以使用队列实现。


BFS

为什么我要在讲广度优先搜索(BFS)之前介绍队列呢?我们先了解一下什么是广度优先搜索。

与深度优先搜索不同,广度优先搜索会先将与起始点距离较近的点搜索完毕,再继续搜索较远的点。

可以把广度优先搜索的过程理解为雷达或者是水波。从起点开始,优先搜索周围距离起点最近的点,然后再由这个点继续扩展其他稍近的点,一层一层扩展。

举个例子。

如果对上图进行深度优先搜索,则访问顺序:A - B - E - F - C - D - G;

如果对上图进行广度优先搜索,则访问顺序:A - B - C - D - E - F - G。

那么这时候,我们对于每个点的搜索就可以通过队列来实现。步骤如下:

  1. 初始的时候把起始点放到队列中,并标记起点访问。
  2. 如果队列不为空,从队列中取出一个元素 x,否则算法结束。
  3. 访问和 x 向链的所有点 v,如果 v 没有被访问,把 v 入队,并标记已经访问。
  4. 重复执行步骤 2

BFS框架模板

根据BFS的搜索过程,我们可以直观的写出BFS的模板如下:

void BFS(起始点) {
    将起始点放入队列中;
    标记起始点访问;
	while (如果队列不为空){
		访问队列首元素;
		删除队列首元素;
		for (x 所有相邻的点){
			if (该点未访问且合法){
				将该点加入队列末尾; 
			}
		} 
	} 
	队列为空,BFS结束; 
}

迷宫游戏问题(BFS解法)

我们之前已经在深度优先搜索那篇接触过了迷宫游戏,并且可以通过深度优先搜索解决迷宫的最短路问题。用 DFS 有很大的弊端,因为 DFS 需要枚举出所有的解法,一旦读取的地图过大,那么可能的解法就会很多,导致效率下降。

反观我们如果使用 BFS 求解迷宫问题,由于 BFS 是分层搜索,因此第一次到达终点时,当前搜索的层数就是最短路径的长度。

 由于 BFS 并不能像 DFS 那样直接传位置与步数(因为 BFS 使用了队列),所以我们需要构建一个结构体来保存这些信息,这一点是需要与BFS区别开的。

//BFS迷宫解法
#include<bits/stdc++.h>
using namespace std;
int n, m;
bool vis[1005][1005];
int dir[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
char maze[1005][1005];
bool in(int x, int y){
	//是否在地图中
	return 0 <= x && x < n && 0 <= y && y < m; 
}
struct node{
	int x, y, d; // x,y表示坐标,d表示步数 
	node(int xx, int yy, int dd){
		//构造函数
		x = xx;
		y = yy;
		d = dd; 
	}
};
int BFS(int sx, int sy){
	queue<node> q;
	q.push(node(sx, sy, 0)); //起点入队
	vis[sx][sy] = true; //标记已经访问
	while(!q.empty()){
		node now = q.front(); //队首节点 
		q.pop(); //弹出队首节点
		for(int i = 0; i < 4; i++){
			//四个方向
			int tx = now.x + dir[i][0];
			int ty = now.y + dir[i][1];
			if(in(tx, ty) && maze[tx][ty] != '*' && !vis[tx][ty]){
				if(maze[tx][ty] == 'T'){
			 		return now.d + 1;
				}else{
					vis[tx][ty] = true;
					q.push(node(tx, ty, now.d + 1));
				}
			}
		}
	}
	return -1; 
} 
int main(){
	cin >> n >> m; 
	//输入 
	for(int i = 0; i < n; i++){
		cin >> maze[i];
	}
	int x, y;
	//找起点 
	for(int i = 0; i < n; i++){
		for(int j = 0; j < m; j++){
			if(maze[i][j] == 'S'){
				x = i;
				y = j;
			}
		}
	}
	cout << BFS(x, y); 
	return 0;
} 

坐标移动问题

在一个长度为 n 的坐标轴上,蒜头君想从 A 点移动到 B 点。他的移动规则如下:

  1. 向前走一步,坐标增加1
  2. 向后走一步,坐标减少1
  3. 跳跃一步,使得坐标乘2

蒜头君不能移动到坐标小于 0 或者大于 n 的位置。蒜头君想知道从 A 点移动到 B 点的最小步数是多少,你能把他计算多少,你能帮他算出来吗?

输入格式:第一行输入三个整数 nAB,分别代表坐标轴长度,起始点坐标,终点坐标。(0\leq AB\leqslant n\leqslant 5000

输出格式:输出一个整数占一行,代表蒜头要走的最少步数。

 分析:按照模板写即可。这里使用了一个小技巧,在队列中使用 pair<int, int> 来简化结构体的创建。对于每一个点,他的相邻的点有三种(+1,-1,*2),所以讨论比较清晰。

//移动
#include<bits/stdc++.h>
using namespace std;
int n, A, B, cnt;
bool vis[5005];
void BFS(){
	int now, step;
	queue<pair<int, int> > q;
	q.push(make_pair(A, 0));
	vis[A] = true;
	while(!q.empty()){
		now = q.front().first;
		step = q.front().second;
		q.pop();
		if(now == B){
			cout << step << endl;
			break;
		}
		if(now + 1 <= n && !vis[now + 1]){
			q.push(make_pair(now + 1, step + 1));
			vis[now + 1] = true;
		}
		if(now - 1  >= 0 && !vis[now - 1]){
			q.push(make_pair(now - 1, step + 1));
			vis[now - 1] = true;
		}
		if(now * 2 <= n && !vis[now * 2]){
			q.push(make_pair(now * 2, step + 1));
			vis[now * 2] = true;
		}
	}
}
int main(){
	cin >> n >> A >> B;
	BFS();
	return 0;
} 

密码锁问题

现在一个紧急的任务是打开一个密码锁。密码由四位数字组成,每个数字从 1 到 9 进行编号。每次可以对任何一位数字加一或减一。当 9 加 1,数字将变为 1,当 1 减 1 时,数字将变为 9。你也可以交换相邻数字,每一个行动记作一步。现在你的任务是使用最小的步骤来打开锁。注意:最左边的数字不与最右边的数字相似。

输入格式:每一行输入四位数字,表示密码锁的初始状态,第二行输入四位数字,表示开锁密码。

输出格式:输出一个整数,表示最小步骤。

分析:这一题的关键在于建立一个四维的 vis 数组来判断是否访问过。

//密码锁
#include<bits/stdc++.h>
using namespace std;
struct node{
	int num[4], step;
} first, last;
int vis[11][11][11][11];
void bfs(){
	node a, next;
	queue<node> q;
	a = first;
	a.step = 0;
	q.push(a);
	memset(vis, 0, sizeof vis);//初始化vis数组 
	vis[a.num[0]][a.num[1]][a.num[2]][a.num[3]] = 1;//标记起点访问 
	while(!q.empty()){
		//队列不为空,继续搜索
		a = q.front();
		q.pop();
		if(a.num[0] == last.num[0] && a.num[1] == last.num[1] && a.num[2] == last.num[2] && a.num[3] == last.num[3]) {
			//bfs出口
			printf("%d", a.step);
			return;
		}
		for(int i = 0; i < 4; i++){	// +1 
			//当前节点临近的点有四个
			next = a;
			next.num[i]++;//增操作
			if(next.num[i] == 10){
				next.num[i] = 1;
			} 
			if(!vis[next.num[0]][next.num[1]][next.num[2]][next.num[3]]){
				//保证在没有访问的前提下,添加入队列 
				vis[next.num[0]][next.num[1]][next.num[2]][next.num[3]] = 1;
				next.step++;
				q.push(next);
			}
		}
		for(int i = 0; i < 4; i++){	// -1 
			//当前节点临近的点有四个
			next = a;
			next.num[i]--;//减操作
			if(next.num[i] == 0){
				next.num[i] = 9;
			} 
			if(!vis[next.num[0]][next.num[1]][next.num[2]][next.num[3]]){
				//保证在没有访问的前提下,添加入队列 
				vis[next.num[0]][next.num[1]][next.num[2]][next.num[3]] = 1;
				next.step++;
				q.push(next);
			}
		}
		for(int i = 0; i < 3; i++){	// 交换 
			//当前节点临近的点有三个
			next = a;
			//交换操作
			next.num[i] = a.num[i + 1];
			next.num[i + 1] = a.num[i];
			if(next.num[i] == 0){
				next.num[i] = 9;
			} 
			if(!vis[next.num[0]][next.num[1]][next.num[2]][next.num[3]]){
				//保证在没有访问的前提下,添加入队列 
				vis[next.num[0]][next.num[1]][next.num[2]][next.num[3]] = 1;
				next.step++;
				q.push(next);
			}
		} 
	}
}
int main(){
	int i, j, t;	
	char s1[10], s2[10];
	scanf("%s%s", &s1, &s2);
	for(int i = 0; i < 4; i++){
		first.num[i] = s1[i] - '0';
		last.num[i] = s2[i] - '0';
	} 
	bfs();
	return 0;
} 

拓展:有一些题目中,可能需要使用字符串来记录状态,而要标记一个字符串,我们可以借助 map。

发布了104 篇原创文章 · 获赞 27 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_41960890/article/details/104884157