广度优先搜索(BFS)(算法笔记)

本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激。


前言

深度优先搜索是一种枚举所有完整路径以遍历所有情况的搜索方法,总是以“广度”作为前进的关键词,采用队列实现。

一、广度优先算法概述

广度优先搜索属于搜索问题的一种,当问题可以被描述为“路径搜索”时,就可以采用搜素问题的所有解的方式来进行解决,所以BFS本质还是暴力。广搜也存在“岔道口”,只是当遇到“岔道口”时,需要将本层所有的岔道都走一遍,然后再走下一层的岔道,符合队列的“先进先出”思想。
广搜和深搜的共同点之一是只有一个起点,也就是说一次搜索只能遍历以一个点为起点的所有路径。
而BFS更适合需要搜索最优解的问题,BFS可以理解为层次遍历,也就是说如果BFS搜索到了一个满足问题的答案,那么这个答案肯定是“距离最近的”,是“最内层的”,那么就不需要继续向外层搜索了,所以BFS对比DFS更适合找最优解。但BFS需要的空间比DFS大,需要将一层的所有可能解都放入队列中,不像DFS那样是走完一条完整路径再去走另一条路(不过递归实现本身占用的内存还是挺大的,除非自己创建栈)。

二、算法设计

BFS设计的关键在于“岔路口”的抽象和“层次”在队列中的具体体现。
算法通用模板:

void BFS(int s) {
	queue <int> q;
	q.push(s);
	while(!q.empty()){
		取出队首元素top;
		访问队首元素top;
		将队首元素出队;
		将top的下一层节点中未曾入队的节点全部入队,并设置为已入队; 
	} 
}

1.数字操作

在这里插入图片描述
岔路口:加1或乘2;
目标:到达指定整数;
采用队列将起点1加入队首元素,将1取出,并访问1(判断是否满足条件),对1 + 1和1 * 2进行判断,如果满足入队条件(<=n)就入队,循环执行该过程直到队列为空(无解,但根据题意肯定有解)或者找到最优解。
记录本层长度,设置计数器,如果访问完本层所有元素则将计数器自加。可以设置一个bool数组记录被访问过的元素,如果元素被访问过就不需要重复访问,大大节省算法时间。
在实际的编码中,我并没有严格按照BFS的思路去写,而是在被访问元素的下一层节点入队时直接进行访问,这样可以节省一层的搜索量,不过我也不知道这样做到底好不好。
完整代码如下:

#include<cstdio>
#include<queue>
#include<algorithm>
using namespace std;

const int MAXN = 100000;
bool inQueue[MAXN + 1] = {false};

int BFS(int n){ //1是起点,+1或*2是岔路口 
	queue <int> qe;
	qe.push(1);   //放入起点
	int step = 0;
	while(!qe.empty()){
		int size = qe.size();   //记录本层长度 
		for(int i = 0; i <= size - 1; i++){ 
			int top = qe.front();  //取出队首元素 
			qe.pop();   
			inQueue[top] = true;
			if(top + 1 == n) return step + 1; //说明下一层
			if(top + 1 <= n && !inQueue[top + 1]) qe.push(top + 1);
			if(top * 2 == n) return step + 1;
			if(top * 2 <= n && !inQueue[top * 2]) qe.push(top * 2);
		} 
		step++;   //层数加一 
	}
	return -1;   //到不了目标点 
}

int main(){
	int n;
	scanf("%d", &n);
	printf("%d", BFS(n));
}

2迷宫最短路径

在这里插入图片描述
岔路口:(x + 1 , y)(x - 1 , y) (x , y + 1) (x , y - 1)
目标:到达终点的最少步数对应的路径
套用BFS的标准模板进行搜索,这道题到没有说不可以返回原来的位置,但可以证明如果存在最短路径,那这条路径一定不存在返回原来的位置的情况,所以可以设置bool散列表判断坐标是否入过队,这样可以防止元素同一元素反复入队,大大节省搜索层数。
关键是如何保存走过的路径,这在DFS里很容易实现,但BFS是层次搜索,不像DFS那样走完一条完整路径才返回去走其他路径,所以并不能采用具体的存储结构存储走过的路径(那样将耗费巨大的存储空间),这里提供两种方法:(1)模拟链表,记录前驱节点位置;(2)DFS + BFS
第二个方法效率较低,这里使用第一种方法,具体操作是开一个类似二叉树的静态二叉链表,只是链表节点中的地址是前驱节点的下标,故只能反向遍历。
具体代码如下:这段代码在网站中运行时有一个数据点运行无结果,但我在DEV中可以运行出正确答案,搞不懂······

#include<cstdio> //关键在于如何记录走过的路径 
#include<queue>
#include<algorithm> 
using namespace std;

const int MAXN = 100;
int number[MAXN][MAXN] = {}; //迷宫
int n , m;  //列数和行数 
int add1[] = {-1 , 1 , 0 , 0};   //两个增量数组 
int add2[] = {0 , 0 , -1 , 1};
bool isqe[MAXN][MAXN] = {true};  //记录是否入队并将起点状态初始化 
struct Node{  //坐标节点 
	int x , y;  //层数 
};
struct node{   //链表节点 
	int prex , prey;
}nd[MAXN][MAXN] , ans[MAXN]; //nd里存的是是静态链表,元素代表前驱节点坐标
// ans是将保存反向遍历的坐标,之后反向遍历 
bool ispush(int x , int y){   //判断是否可以入队 
	if(x > n - 1 || x < 0 || y > m - 1 || y < 0) return false;  //超出边界 
	if(number[x][y] == 1) return false;  //墙壁
	if(isqe[x][y] == true) return false; //入过队
	return true; 
}

node BFS(){
	queue <Node> qe;
	Node front = {0 , 0};  //起点位置设置为0,0 
	qe.push(front);
	nd[0][0] = {-1 , -1}; //初始化起点 
	while(!qe.empty()){
		Node top = qe.front();
		qe.pop();
		if(top.x == n - 1 && top.y == m - 1) return nd[top.x][top.y]; //返回尾结点 
		for(int i = 0; i <= 3; i++){   //枚举下一层4个岔路口 
			int newx = top.x + add1[i];
			int newy = top.y + add2[i];
			if(ispush(newx , newy)){    //是否可以入队 
				Node temp = {newx , newy};
				qe.push(temp);
				isqe[newx][newy] = true; //入队 
				nd[newx][newy] = {top.x , top.y};
				if(newx == n - 1 && newy == m - 1) return nd[newx][newy];   //直接访问入队元素
			}
		}
	} 
}

int main(){
	scanf("%d%d", &n , &m);
	for(int i = 0; i <= n - 1; i++)
		for(int j = 0; j <= m - 1; j++)
			scanf("%d", &number[i][j]);
	node tail = BFS();
	int num = 0;
	ans[num++] = {n , m};  //把终点放入 
	while(tail.prex != -1) {
		ans[num++] = {tail.prex + 1 , tail.prey + 1};
		tail = nd[tail.prex][tail.prey];
	}
	for(int i = num - 1; i >= 0; i--)
		printf("%d %d\n", ans[i].prex , ans[i].prey);
}

3跨步迷宫

在这里插入图片描述
岔路口:(x + 1 , y)(x - 1 , y) (x , y + 1) (x , y - 1)(x + 2 , y)(x - 2 , y) (x , y + 2) (x , y - 2)
目标:求起点到终点的最小步数
与基本迷宫问题大致相同,套用BFS模板更改增量数组即可,需要注意的是在走两步时需要判断这两步上是否存在墙壁,可以新建两个临时变量存储中间位置的坐标,判断中间位置是否是墙壁即可。(注:如果中间位置入队了,还是可以走的,不过只能是跨过去,只要保证实际落点位置没有入队即可)
完整代码如下:

#include<cstdio>  
#include<queue>
#include<vector>
#include<algorithm> 
using namespace std;

const int MAXN = 100;
int number[MAXN][MAXN] = {}; //迷宫
int n , m;  //列数和行数 
vector <int> vt;    //记录路径 
int add1[] = {-1 , 1 , 0 , 0 , -2 , 2 , 0 , 0};   //两个增量数组 
int add2[] = {0 , 0 , -1 , 1 , 0 , 0 , -2 , 2};
bool isqe[MAXN][MAXN] = {true};  //记录是否入队并将起点状态初始化 
struct Node{
	int x , y , step;  //层数 
};

bool ispush(int x , int y){   //判断是否可以入队 
	if(x > n - 1 || x < 0 || y > m - 1 || y < 0) return false;  //超出边界 
	if(number[x][y] == 1) return false;  //墙壁
	if(isqe[x][y] == true) return false; //入过队
	return true; 
}

int BFS(){
	queue <Node> qe;
	Node front = {0 , 0 , 0};  //起点位置设置为0,0 
	qe.push(front);
	while(!qe.empty()){
		Node top = qe.front();
		qe.pop();
		if(top.x == n - 1 && top.y == m - 1) return top.step;
		for(int i = 0; i <= 7; i++){   //枚举下一层8个岔路口 
			int newx = top.x + add1[i];
			int newy = top.y + add2[i];
			int halfnewx = top.x + add1[i] / 2;
			int halfnewy = top.y + add2[i] / 2;
			if(ispush(newx , newy) && number[halfnewx][halfnewy] == 0){    //是否可以入队 
				Node temp = {newx , newy , top.step + 1};
				qe.push(temp);
				isqe[newx][newy] = true; //入队 
				if(newx == n - 1 && newy == m - 1) return temp.step;   //直接访问入队元素
			}
		}
	} 
	return -1;
}

int main(){
	scanf("%d%d", &n , &m);
	for(int i = 0; i <= n - 1; i++)
		for(int j = 0; j <= m - 1; j++)
			scanf("%d", &number[i][j]);
	printf("%d", BFS());
}

4中国象棋—马—无障碍

在这里插入图片描述
在这里插入图片描述
岔路口:以下为x轴正方向,以右为y轴正方向建立坐标,8个岔路口表示为:(x + 1 , y - 2)(x + 2 , y - 1) (x + 2, y + 1) (x - 1 , y + 2)(x + 1 , y + 2)(x - 2 , y - 1) (x - 2 , y + 1) (x - 1 , y - 2).
目标:求到每一点的最小步数
可以证明BFS搜索到的每一个坐标的层数就是从起点到达这个点的最少步数,相当于全遍历,所以开一个数组记录到达坐标的层数即可。注意不要采用循环遍历终点的方式求最优解,会超时,并且全遍历就不需要在入队时提前访问了。
完整代码如下:

#include<cstdio> //关键在于如何记录走过的路径 
#include<queue>
#include<algorithm> 
#include <cstring>
using namespace std;

const int MAXN = 100;
int ans[MAXN][MAXN] = {};
int n , m;  //列数和行数  
int sx , sy;  //起点和终点 
int add1[] = {-1 , 1 , 2 , 2 , 1 , -2 , -2 , -1};   //两个增量数组 
int add2[] = {2 , -2 , -1 , 1 , 2 , -1 , 1 , -2};
bool isqe[MAXN][MAXN] = {false};  //记录是否入队
struct Node{
	int x , y , step;  //层数 
};

bool ispush(int x , int y){   //判断是否可以入队 
	if(x > n - 1 || x < 0 || y > m - 1 || y < 0) return false;  //超出边界 
	if(isqe[x][y] == true) return false;
	return true; 
}

void BFS(){ //参数为起点和终点 
	memset(ans, -1, sizeof(ans));
	queue <Node> qe;
	Node front = {sx - 1, sy - 1, 0};  //起点位置 
	qe.push(front);
	while(!qe.empty()){
		Node top = qe.front();
		qe.pop();
		ans[top.x][top.y] = top.step;
		for(int i = 0; i <= 7; i++){   //枚举下一层8个岔路口 
			int newx = top.x + add1[i];
			int newy = top.y + add2[i];
			if(ispush(newx , newy)){    //是否可以入队 
				Node temp = {newx , newy , top.step + 1};
				qe.push(temp);
				isqe[newx][newy] = true; //入队 
			}
		}
	} 
	return;
}

int main(){
	scanf("%d%d", &n , &m);
	scanf("%d%d", &sx , &sy);
	isqe[sx - 1][sy - 1] = true;
	BFS();   //别忘了
	for(int i = 0; i <= n - 1; i++){
		for(int j = 0; j <= m - 1; j++){
			printf("%d", ans[i][j]);
			if(j < m - 1) printf(" ");
		}
		printf("\n");
	}	
}

三、备注

1.BFS相当于层次遍历,是一层一层遍历问题的解;
2.体会层次在队列中的体现;
3.两种记录层数的方法:(1)将元素信息和层数绑定在一起,这样元素的所在层数可以随着访问元素而动态变化;(2)从第一层开始循环记录当层的全部元素即队列的元素个数,当遍历完本层元素后将计数器自加;
4.搜索算法就是针对非线性路径问题的全遍历,如果问题的路径是线性的、是1对1的,就只需要按照固定的逻辑关系进行遍历即可。

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瓦耶_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值