1. 程式人生 > 其它 >圖論學習筆記——SPFA判斷負環

圖論學習筆記——SPFA判斷負環

演算法描述

有一個n個點、m條邊的有向/無向有權圖,判斷該圖中有沒有負環。

注意:圖並不一定所有點都是聯通的

負環的定義:圖中形成了一個環,且環上面的邊權之和為負數。

例題:AcWing 852. spfa判斷負環

分析與解法

負環是在寫最短路(尤其是 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;
}