算法描述
有一个n个点、m条边的有向/无向有权图,判断该图中有没有负环。
注意:图并不一定所有点都是联通的。
负环的定义:图中形成了一个环,且环上面的边权之和为负数。
分析与解法
负环是在写最短路(尤其是 SPFA)的问题中需要考虑的问题,它会导致程序陷入死循环,程序里需要避免这个问题。
因为出现了负数,所以 Dijkstra 算法可以排除了,于是转向效率略低但可以处理负数的 SPFA。
在SPFA算法中,遇见了负环会导致最短路的值会不断减小。有一些点会不断更新入队,队列永远不为空,可以从这里找到突破口。
不难想到可以增加一个统计每个结点入队次数的数组,如果一个点入队超过了 \(n\) 次(也就是连正常情况下最多的入队次数都超过了),说明有一个点被重复使用,就判定有负环。
还有一种方法:统计某一个点到该点的最短路目前包含多少条边,每次满足三角行不等式时更新这个值。如果一条最短路上包含了超过 \(n - 1\) 条边,说明有一条边被重复使用,有负环。
但这两个思路都有一个缺陷:由于图并不保证两点之间一定能到达,如果从任意一点向任意一点的最短路中没有出现负环(就像以下这个情况),程序就会出错:
如图,如果求的是1到其他点的最短路,则不会出现负环,会报错。
解法1:
从每个点跑一次SPFA,这样肯定能找出负环。
一般复杂度 \(O(NM)\) ,最差复杂度 \(O(N^2M)\),难以接受。
解法2:
可以再建立一个 \(0\) 号结点,我们称它为“虚拟源点”。
把它向所有节点连一条边权为 \(0\) 的边,然后从 \(0\) 号点向其他点跑最短路,在一开始就可以将所有点入队列,通过所有结点来更新,这样再用上面两种方式都可以判定出负环。
具体看下图这个例子:在上面的原图上加了一个 \(0\) 点,可以手模一下,就会发现可以判出负环了。
Code : AcWing 852
// by pjx Aug.
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>
#include <stack>
#define REP(i, x, y) for(register int i = x; i < y; i++)
#define rep(i, x, y) for(register int i = x; i <= y; i++)
#define PER(i, x, y) for(register int i = x; i > y; i--)
#define per(i, x, y) for(register int i = x; i >= y; i--)
#define lc (k << 1)
#define rc (k << 1 | 1)
using namespace std;
const int N = 1E4 + 5;
int n, m;
struct node{
int v, w;
};
vector <node> g[N];
queue <int> que;
int cnt[N];
int b[N], dis[N];
int main()
{
cin >> n >> m;
rep(i, 1, n)
{
g[0].push_back({i, 0});//建立“虚拟源点”
que.push(i);
b[i] = 1;
}
rep(i, 1, m)
{
int x, y, z;
cin >> x >> y >> z;
g[x].push_back({y, z});
}
b[0] = 1;
while(!que.empty())
{
int k = que.front();
que.pop();
b[k] = 0;
for(int j = 0; j < g[k].size(); j++)
{
int v = g[k][j].v;
int w = g[k][j].w;
if(dis[k] + w < dis[v])
{
dis[v] = dis[k] + w;
cnt[v] = cnt[k] + 1;//这里用的是第二种判负环的方式
if(cnt[v] == n)//如果最短路走过的边数超过了n,则判定
{
cout << "Yes";
return 0;
}
if(!b[v])
{
b[v] = 1;
que.push(v);
}
}
}
}
cout << "No";
return 0;
}