点的层次BFS
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 a 和 b,表示存在一条从 a 走到 b 的长度为 1 的边。输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。数据范围
1≤n,m≤105
输入样例:
4 5
1 2
2 3
3 4
1 3
1 4
输出样例:
1 -
题目来源:https://www.acwing.com/problem/content/849/
题目分析:
- 求1号点到n号点的最少步数,马上确定是BFS
- 点数n == 边数 m == 105,稀疏图,静态邻接表即可,又是有向图,insert()一次即可
- 注意这句话:图中可能存在重边和自环
重边:虽然不影响两点距离,但是可能造成BFS队列死循环,需要用st[N]标记数组标记是否走过
自环:st[N]数组解决了死循环问题,但是这句话引入了代码写作的隐形问题,下面讲 - 下面我们来讲述最纯粹的BFS
算法原理:
模板算法:
本题特殊点:
1. 回顾静态邻接表
- 静态链表中的所有节点依靠idx索引统一指挥
- h[i]存储着与节点i连接的其他节点构成的链表的头节点的idx索引
- ne[i]存储idx == i的节点的下一节点
- val[i]才是存储着图论中的节点
- 由于val[], ne[]统一指挥于idx,h[i]存储着idx
所以如果要添加附属信息,需要先明确,是为静态链表中的节点添加信息,还是为图论中的节点添加信息 - 若为链表中的节点添加信息,则开数组,以idx为索引
- 若为图论中的节点添加信息,则开数组,以val[idx]为索引
2. 回顾BFS
- 依托于队列queue,队列头hh,队列尾tt
由于我们一般也不采用循环队列,所以直接初始化tt = hh = 0
当队列为空时,tt < hh;不空则hh <= tt - 队列存储内容:
与静态邻接表不同,队列存储的是纯粹的图论中的节点,所以que[tt++] = val[i];而不是直接本质为idx的i - BFS写作只有三步:
- 先向队列放入图论中起始节点
- while(队列不空),队头出队
- 将图中队头节点的子节点入队
3. 注意点
-
标记数组,st[N] 或 dis[N],防止死递归
-
BFS适用情况:BFS求的是最少步数,不是最短距离,当边的权重不同时,这一点差异尤其明显
-
当图中存在环结构时,题目难度上升
本题中仅有自环,由于自环的出现,只能采用出队的队头作为判断是否到达终点的标准
因为若终点就是起点,则起点作为队头入队后马上就被dis[]标记,不可能再作为队尾入队了,所以我们只能在队头出队处判断是否达到终点
代码实现:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int h[N], val[N*2], ne[N*2], idx;
int n, m;
void insert(int x, int y){
val[idx] = y;
ne[idx] = h[x];
h[x] = idx++;
}
int dis[N];
int que[N], tt, hh;
int bfs(int x){
memset(dis, -1, sizeof(dis));
que[tt++] = x;
dis[x] = 0;
while(hh <= tt){
int t = que[hh++];
if (t == n) return dis[n];
for(int i = h[t]; i!=-1; i=ne[i]){
if (dis[val[i]] == -1){
dis[val[i]] = dis[t] + 1;
que[tt++] = val[i];
}
}
}
return -1;
}
int main(){
memset(h, -1, sizeof(h));
cin >>n >>m;
while(m--){
int x, y;
cin >>x >>y;
insert(x, y);
}
cout<< bfs(1);
return 0;
}
代码误区:
1.为什么队头出队时检验是否达到终点?队尾入队时可以吗?
- 不可能,假设现在只有1个点,1条自环边,起点即终点
- 当代码写为这样时:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int h[N], val[N*2], ne[N*2], idx;
int n, m;
void insert(int x, int y){
val[idx] = y;
ne[idx] = h[x];
h[x] = idx++;
}
int dis[N];
int que[N], tt, hh;
int bfs(int x){
memset(dis, -1, sizeof(dis));
que[tt++] = x;
dis[x] = 0;
while(hh <= tt){
int t = que[hh++];
for(int i = h[t]; i!=-1; i=ne[i]){
//由于只有一个点,所以在上面dis[终点] = 0;了,此处不可能进入判断,则不可能return dis[终点] == 0
if (dis[val[i]] == -1){
dis[val[i]] = dis[t] + 1;
que[tt++] = val[i];
if (t == n) return dis[n];
}
}
}
//起点 = 终点时,return -1作为距离
return -1;
}
int main(){
memset(h, -1, sizeof(h));
cin >>n >>m;
while(m--){
int x, y;
cin >>x >>y;
insert(x, y);
}
cout<< bfs(1);
return 0;
}
本篇感想:
- 这篇图论bfs应该放在迷宫问题的后面,状态压缩的前面,这样比较友好。
- 进入最短路径5大算法和最小生成树两大算法时,达到 筑基境-中期
- 看完本篇博客,恭喜已登 《筑基境-初期》
距离登仙境不远了,加油