本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激。
前言
深度优先搜索是一种枚举所有完整路径以遍历所有情况的搜索方法,总是以“广度”作为前进的关键词,采用队列实现。
一、广度优先算法概述
广度优先搜索属于搜索问题的一种,当问题可以被描述为“路径搜索”时,就可以采用搜素问题的所有解的方式来进行解决,所以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的,就只需要按照固定的逻辑关系进行遍历即可。