圖論學習筆記——SPFA判斷負環
阿新 • • 發佈:2021-08-09
演算法描述
有一個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; }