负环SPFA
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。输出格式
如果图中存在负权回路,则输出 Yes,否则输出 No。数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes -
题目来源:https://www.acwing.com/problem/content/854/
题目分析:
- 图论:有向图 m < n2,稀疏图
- 负边权 -> bellman-ford / SPFA
- 判断是否存在负权回路 -> 负环SPFA
算法原理:
模板算法:
- 传送门:bellman-ford
- 传送门:朴素SPFA
负环SPFA:
1. 负环可能存在哪里?
- 情况1:非连通部分,如求a到e的最短路径,但是f <->g构成的孤立系统内含有负环
- 情况2:连通部分的非目标路径:如求a到e的最短路径,但是a->c->d构成了负环
- 情况3:连通部分的目标路径:如求a到b的最短路径,每次多走一圈b->c->d则最短路径长-2
- 启示:
最短路径问法有两种- 从a到b的最短路径中是否存在负环
- 图中是否存在负环
- 若是问题1,则只需要将a点作为起点
- 若是问题2,需要将图中所有点尝试为起点
2. 存储形式:
-
图:邻接表
-
队列queue
-
在队列中标志,st[N]
-
到单源的距离:dis[N]
-
到某点的最短路径中点数:count[x]
若通过x点更新了y点到单源的距离,
则count[y] = count[x]+1; 意为到x点的最短路径中添加y点就是到y点的最短路径初始认为每个点都是到自己的最短路径中的一点,count[N] = 1;
3. 负环检测原理:
- 负环包括两个含义:环 & 绕环一周权值为负
-
环:
已知图中共有n点
若从1号点到x号点的最短路径中含有n+1个点
则不管前n个点中是否有环,第n+1个点必然和原来的某点相同,构成了环
-
负:
负环的绕环一周权值为负是一个独立的性质
不可能因为起点改变,绕环一周就变正了
所以如上图,不论是从a点到b点,还是从c点到b点,
只要含有b点,则每多走一圈bcd,路径长-2
- 综上两点,我们得出判断路径中是否含有负环的方法:
- 到x点的最短路径中含有n+1个点,以构成环
- 每次走一遍环,任意点到x点的距离减少,以满足负
- 要判断图中是否含有负环
则需要将所有点都当作出发点,走完所有路径
但是由于负环的独立性,所有路径中的点都可以到指定起点的距离作为负的判断
只要指定的起点和该点在同一连通分量内即可 - 本题不考虑非连通图的dis[]衡量
写作步骤:
1. 初始化:
- dis[单源] = 0; st[单源] = 1;
- 各点count[i] = 1;
- 所有点都当作起点入队,以判断图中是否含有连通分量
2. 出队&入队:
- 队头出队,遍历队头节点出边的终点
- 可通过队头拉近dis[]的终点从队尾入队
同时count[终点] = count[队头]+1;
3. 用count判断负环:
- 当有一count[终点] > n,则根据抽屉原理,图中存在负环
代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 10010;
int n,m;
int h[N],val[N],ne[N],w[N],idx;
void add(int x, int y, int d){
val[idx] = y;
w[idx] = d;
ne[idx] = h[x];
h[x] = idx++;
}
int dis[N], cnt[N];
bool st[N];
int spfa(){
memset(dis,0x3f,sizeof(dis));
dis[1] = 0;
queue<int> q;
for(int i=1; i<=n; i++){
q.push(i);
st[i] = true;
cnt[i] = 1;
}
while(q.size()){
int t = q.front();
q.pop();
st[t] = 0;
for(int i=h[t]; i!=-1; i=ne[i]){
if(dis[val[i]] > dis[t]+w[i]){
dis[val[i]] = dis[t]+w[i];
cnt[val[i]] = cnt[t]+1;
if(cnt[val[i]]>n)
return 1;
if(!st[val[i]])
q.push(val[i]);
st[i] = true;
}
}
}
return 0;
}
int main(){
cin >>n >>m;
memset(h,-1,sizeof(h));
while(m--){
int a=0, b=0, c=0;
cin >>a >>b >>c;
add(a,b,c);
}
int t = spfa();
if(t == 1) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
代码误区:
1. 注意是问图中存在负环,还是问路径中存在负环
- 问图中存在负环,则将图中所有点放入队列
- 问路径中存在负环,则将仅仅将路径起点放入队列
2. 注意图是连通图还是非连通图
- 对于负环的环的判断,是否连通无影响,只是多走了几次负环
- 对于负环的负的判断,由于要借助负环的独立性来指定所有点到起点的dis[]
所以每个非连通部分都要指定自己的起点
3. 注意count[N]数组的含义
- 若通过xy这条边,将y到起点的距离缩短为x到起点的距离+xy的距离
则视为y的最短路径 就是x的最短路径 外加一点
count[y] = count[x]+1; - 由于每个点必然是到自己的最短路径中的点
所以count[i]初始化为1 - 当某点最短路径中点数count[x] > n时
一方面是因为最短路径又变短了,所以到x的最短路径count[]又可以加入点
另一方面是因为路径中含有了n+1个点,不论前n个点是否有环,第n+1个点的加入必然出现了环
本篇感想:
-
SPFA判断负环存在本身简单,就三步:
- 初始化,dis,st,count,起点入队
- 队头出队,距离缩短的点队尾入队
- 判断入队的点的最短路径中点数是否>n
-
与之相比更值得注意的是负环判断的推论,以及问题是针对图OR路径,图是连通图OR非连通
-
第五十篇神机百炼了,美图庆祝一下吧
-
看完本篇博客,恭喜已登 《筑基境-中期》
距离登仙境不远了,加油