上文复习了深度优先搜索,我们这篇文章来看看广度优先搜索(BFS)。不要着急,我们先复习一下队列。
队列(Queue)
在讲广度优先搜索时,我们先来复习一下队列(Queue),这里不把队列作为介绍的重点,基本功扎实的小伙伴可以跳过这一部分。
队列是一种数据结构,也就是存放、操作数据的方式,重点需要掌握的是他的逻辑——“先进先出”。跟我们生活中排队上车一样,先进入队列的,先刷卡上车,后进入队列的人只能等前面的人离开队列,才能向前移动。
队列的主要操作:
- 入队(push)
- 出队(pop)
- 判断队列是否为空(empty)
- 统计队列元素个数(size)
- 访问队首元素(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。
那么这时候,我们对于每个点的搜索就可以通过队列来实现。步骤如下:
- 初始的时候把起始点放到队列中,并标记起点访问。
- 如果队列不为空,从队列中取出一个元素 ,否则算法结束。
- 访问和 向链的所有点 ,如果 没有被访问,把 入队,并标记已经访问。
- 重复执行步骤 。
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;
}
坐标移动问题
在一个长度为 的坐标轴上,蒜头君想从 点移动到 点。他的移动规则如下:
- 向前走一步,坐标增加1
- 向后走一步,坐标减少1
- 跳跃一步,使得坐标乘2
蒜头君不能移动到坐标小于 0 或者大于 的位置。蒜头君想知道从 点移动到 点的最小步数是多少,你能把他计算多少,你能帮他算出来吗?
输入格式:第一行输入三个整数 ,,,分别代表坐标轴长度,起始点坐标,终点坐标。(,)
输出格式:输出一个整数占一行,代表蒜头要走的最少步数。
分析:按照模板写即可。这里使用了一个小技巧,在队列中使用 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;
}
密码锁问题
现在一个紧急的任务是打开一个密码锁。密码由四位数字组成,每个数字从 到 进行编号。每次可以对任何一位数字加一或减一。当 加 ,数字将变为 ,当 减 时,数字将变为 。你也可以交换相邻数字,每一个行动记作一步。现在你的任务是使用最小的步骤来打开锁。注意:最左边的数字不与最右边的数字相似。
输入格式:每一行输入四位数字,表示密码锁的初始状态,第二行输入四位数字,表示开锁密码。
输出格式:输出一个整数,表示最小步骤。
分析:这一题的关键在于建立一个四维的 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。