广度优先搜索,广度优先,深度次要。
与DFS不同是,BFS不再嵌套循环的思想,而是采用辅助队列来辅助实现广度与深度上的推进。
目录
例题
此题是一道十分经典且基础的BFS求最短路径问题。
算法的代码逻辑
算法的代码骨架:循环+辅助队列
算法的代码逻辑:
BFS模版较为固定,只要弄懂其中的逻辑,相似题目代码的写法大差不差
起点入队,循环开始。,每从队中取出一个元素,然后对该元素在图中上下左右四个方向的相邻元素进行检查,若该条路能走,令该元素入队,等它成为队头后,令其出队,再对其上下左右四个方向的相邻元素进行检查。若该路不能走,直接pass掉,不令该元素入队。如此循环往复,直到终点元素出队,BFs结束,break掉循环。
注意的是,此处的元素入队是指当前元素的x,y坐标的值入队,具体请看下列详情。
使用队列的原因
利用队列先进先出的特点。当前深度的所有节点均检查完并出队列之后,再去检查下一层的节点,再出队列。
重点:建系、方向向量、mark数组
BFS解决图的最短路径问题,往往会涉及到建系与方向向量。
建系作用:确定当前节点在图中的具体位置,一般可用(x,y)来表示当前位置。
单位方向向量作用:利用向量的坐标运算来获取当前节点上下左右相邻元素的位置
(x-1,y)(x,y+1)(x+1,y)(x,y-1)
mark数组:
0表示未走过,1表示已走过。
作用(重点):
1.记录该点有没有走过,防止已经出队的节点重复入队,同时可以在一定程度上进行剪枝优化(即令一些已经走远路的DFS路线搜索中途截断,节约时间)。
2.记录从起点到该点最短路径为多少。
在所有能够到达该点的路径中,最先到达该点的路径是最短的。
也正因如此,从起点出发到达终点的若干路径中,最短路径是——从起点开始,每次走的路都是到达某点最短的一条路径,直到到达终点。(其中隐隐嵌套了积分的思想)
int map[110][110];
//注意:此处不能直接使用cout计数器来统计最短步数,因为代码在执行时会不可避免地出现cout重复++
int mark[110][110] = { 0 };
int x[4] = {-1,0,1,0};//模拟x方向上的方向向量
int y[4] = {0,1,0,-1};//模拟y方向上的方向向量
建两个队列的原因
由于c语言的局限性,队列的元素只能是一个值,而不能像cpp那般是一对值,因此必须要建两个辅助队列。
一个队列存坐标x值,一个队列存坐标y值
//队列难点:
//1. 封装一个函数来创建一个队列,结构体指针来指代一个队列
//2.在主函数中,直接利用结构体来声明一个队列,可用结构体变量来指代一个队列
pQueue queuex = creatAndIniteQueue();
pQueue queuey = creatAndIniteQueue();
pushElement(queuex,0);
pushElement(queuey,0);
此路入队的判定条件
1.该路能走,即坐标系中该点对应的map元素值为0;
2.该路未被走过,即坐标系中该点对应的mark元素值为0;
while (isEmpty(queuex)!=1 ){
int dx = popElement(queuex);
int dy = popElement(queuey);
for (int i = 0; i < 4;++i) {
int nex = dx + x[i], ney = dy + y[i];
if (nex == n-1 && ney == m-1) {
int ans = mark[dx][dy] + 1;
printf("%d",ans);
return 0;
}
if ( map[nex][ney] == 0 && mark[nex][ney] == 0 && nex>= 0 && ney >=0
&& nex <= n-1 && ney <= m-1
)
{
//程序进到这里说明此路可通
mark[nex][ney] = 1;
//每次走的路都是相邻的路,因此每次只走一步
mark[nex][ney] = mark[dx][dy] + 1;
pushElement(queuex,nex);
pushElement(queuey,ney);
}
}
}
完整代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
//c语言并没有现成的队列使用,在比赛中需要手撸一个队列出来
typedef struct {
int* nums;
int head;
int tail;
int capacity;
}Queue,* pQueue;
//这里创建队列空间出错了: 函数内成功给临时变量pqe创建了空间,但出函数后,这个pqe就销毁了,并且更为严重的是,pqe指向的空间未被销毁,这导致了该空间没有指针指向,纯纯的空间浪费
//改错:函数体创建了该空间后,一定要记得将该地址返回给主调函数,让这块空间能够被外部接收
pQueue creatAndIniteQueue() {
pQueue pqe;
pqe = (pQueue)malloc(sizeof(Queue));
pqe->head = 0;//头指针代表队头
pqe->tail = 0;//尾指针代表队尾
pqe->capacity = 200;
pqe->nums = (int*)malloc(sizeof(int) * pqe->capacity);
return pqe;
}
void pushElement(pQueue pqe,int target) {
//
if ((pqe->tail + 1)%pqe->capacity == pqe->head) {
printf("队列已满!!");
return;
}
pqe->nums[pqe->tail] = target;
pqe->tail = (pqe->tail + 1) % pqe->capacity;
}
int popElement(pQueue pqe) {
if (pqe->tail == pqe->head) {
printf("队列已空!");
return;
}
int tmp = pqe->nums[pqe->head];
pqe->head = (pqe->head + 1) % pqe->capacity;
return tmp;
}
int isEmpty(pQueue pqe) {
if (pqe->tail == pqe->head)
return 1;
return 0;
}
int map[110][110];
//注意:此处不能直接使用cout计数器来统计最短步数,因为代码在执行时会不可避免地出现cout重复++
int mark[110][110] = { 0 };
int x[4] = {-1,0,1,0};//模拟x方向上的方向向量
int y[4] = {0,1,0,-1};//模拟y方向上的方向向量
int main() {
int n = 0, m = 0;
scanf("%d %d",&n,&m);
for (int i = 0; i < n;++i) {
for (int j = 0; j < m;++j ) {
scanf("%d",&map[n][m]);
}
}
//队列难点:
//1. 封装一个函数来创建一个队列,结构体指针来指代一个队列
//2.在主函数中,直接利用结构体来声明一个队列,可用结构体变量来指代一个队列
pQueue queuex = creatAndIniteQueue();
pQueue queuey = creatAndIniteQueue();
pushElement(queuex,0);
pushElement(queuey,0);
//判定条件:1.该路能走,即元素为0 2.该路未被走过,即mark中该处值为0;
while (isEmpty(queuex)!=1 ){
int dx = popElement(queuex);
int dy = popElement(queuey);
for (int i = 0; i < 4;++i) {
int nex = dx + x[i], ney = dy + y[i];
if (nex == n-1 && ney == m-1) {
int ans = mark[dx][dy] + 1;
printf("%d",ans);
return 0;
}
if ( map[nex][ney] == 0 && mark[nex][ney] == 0 && nex>= 0 && ney >=0
&& nex <= n-1 && ney <= m-1
)
{
//程序进到这里说明此路可通
mark[nex][ney] = 1;
//每次走的路都是相邻的路,因此每次只走一步
mark[nex][ney] = mark[dx][dy] + 1;
pushElement(queuex,nex);
pushElement(queuey,ney);
}
}
}
return 0;
}