SPFA算法詳解


前置知識:Bellman-Ford算法

前排提示:SPFA算法非常容易被卡出翔。所以如果不是圖中有負權邊,盡量使用Dijkstra!(Dijkstra算法不能能處理負權邊,但SPFA能)

前排提示*2:一定要先學Bellman-Ford!

0.引子

在Bellman-Ford算法中,每條邊都要松弛\(n-1\)輪,還不一定能松弛成功,這實在是太浪費了。能不能想辦法改進呢?

非常幸運,SPFA算法能做到這點。(SPFA又名“Bellman-Ford的隊列優化”,就是這個原因。)

1.基本思想

先說一個結論:只有一個點在上一輪被松弛成功時,這一輪從這個點連出的點才有可能被成功松弛。

為什么?顯而易見

好吧其實我當初也花了不少時間理解這玩意

松弛的本質其實是通過一個點中轉來縮短距離(如果你看了前置很容易理解)。所以,如果起點到一個點的距離因為某種原因變小了,從起點到這個距離變小的點連出的點的距離也有可能變小(因為可以通過變小的點中轉)。(通讀三遍再往下看)

所以,可以在下一輪只用這一輪松弛成功的點進行松弛,這就是SPFA的基本思想。

2.用隊列實現

我們知道了在下一輪只用這一輪松弛成功的點進行松弛,就可以把這一輪松弛成功的點放進隊列里,下一輪只用從隊列里取出的點進行松弛。

為什么是隊列而不是其他的玄學數據結構?因為隊列具有“先進先出,后進后出”的特點,可以保證這一輪松弛的點不會在這一輪結束之前取出。

干說可能不太理解,所以還是舉個栗子吧。

這又是之前的有向圖,但是這次我們要用SPFA跑。

最開始,我們要把\(1\)號點放進隊列(為什么要這樣?先往下看)。\(dis\)數組和隊列是這個亞子的:

\(i\) \(dis[i]\)
\(1\) \(0\)
\(2\) \(\infty\)
\(3\) \(\infty\)
\(4\) \(\infty\)
queue 1

\(1\)號點進行松弛(就是\(1\)號到\(1\)號再到目標點):

\(i\) \(dis[i]\)
\(1\) \(0\)
\(2\) \(1\)
\(3\) \(5\)
\(4\) \(\infty\)
queue 2 3

\(2,3\)號點被松弛成功了,把它們加入到隊列里。

\(1\)號點被用過了,把它扔掉。(工具點石錘)

\(2\)號點進行松弛:

\(i\) \(dis[i]\)
\(1\) \(0\)
\(2\) \(1\)
\(3\) \(5\)
\(4\) \(3\)
queue 3 4

\(4\)號點被松弛成功了,把它們加入到隊列里。

\(2\)號點被用過了,把它扔掉。

\(3\)號點進行松弛:

\(i\) \(dis[i]\)
\(1\) \(0\)
\(2\) \(1\)
\(3\) \(5\)
\(4\) \(3\)
queue 4

沒有點被松弛成功。

\(3\)號點被用過了,把它扔掉。

\(4\)號點進行松弛:

\(i\) \(dis[i]\)
\(1\) \(0\)
\(2\) \(1\)
\(3\) \(4\)
\(4\) \(3\)
queue 3

\(3\)號點被松弛成功了,把它們加入到隊列里。

\(4\)號點被用過了,把它扔掉。

\(3\)號點進行松弛:

\(i\) \(dis[i]\)
\(1\) \(0\)
\(2\) \(1\)
\(3\) \(4\)
\(4\) \(3\)
queue

沒有點被松弛成功。

\(3\)號點被用過了,把它扔掉。

現在隊列為空(也就是能松弛的都松弛了),算法結束。

3.Code

SPFA的具體實現,推薦結合上面的栗子食用。

#include <bits/stdc++.h>
using namespace std;
#define MAXN 10005
#define INF 0x7fffffff
int n,m,s,dis[MAXN];
vector<pair<int,int> > g[MAXN];//用vector存圖,但是據說鏈式前向星更快
void spfa(){
    queue<int> q;
    q.push(s);//把初始點加入隊列
    fill(dis+1,dis+1+n,INF);//因為一開始所有點都到不了,所以初始化為INF
    dis[s]=0;//自己到自己肯定距離為0
    while(!q.empty()){
        int u=q.front();
        q.pop();//從隊列里取出第一個元素
        for(int i=0;i<g[u].size();i++){
            int v=g[u][i].first,w=g[u][i].second;
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                q.push(v);
            }
            //如果能松弛成功,那么松弛,把松弛成功的目標點放入隊列
        }
    }
}
int main(){
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back(make_pair(v,w));
    }//輸入,建圖
    spfa();
    for(int i=1;i<=n;i++){
        printf("%d ",dis[i]);
    }//輸出
    return 0;
}

這個代碼能夠ACP3371,但是我還是推薦你自己碼一遍。

4.特點

  • 能夠處理有負權邊的圖,但是隔壁Dijkstra不行。
  • 在有負環的情況下,不存在最短路,因為不停在負環上繞就能把最短路刷到任意低。但是SPFA能夠判斷圖中是否存在負環,具體方法為統計每個點的入隊次數,如果有一個點入隊次數$ \ge n $,那么圖上存在負環,也就不存在最短路了。
  • 什么?你不知道什么叫負環?下面的就是。

就是一個環,邊權和是負。一般用一個名為菜雞算法的算法判斷

——Ynoi

  • SPFA的時間復雜度是\(O(km)\)\(k\)是每一個節點的平均入隊次數,經過實踐,\(k\)一般為\(4\),所以SPFA通常情況下非常高效。但是SPFA非常容易被卡出翔,最壞情況下會變成\(O(nm)\) 所以如果能用隔壁Dijkstra盡量不要用SPFA。至於具體怎么卡,據說是這樣的:

(這種圖據說叫菊花圖,能欺騙SPFA多次讓點進入隊列,所以\(k\)會變得非常大(上限為\(n\))。)

5.你都看到這了就不點一個贊嗎?

這個最重要了qwq


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM