前置知識: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