1. 程式人生 > >探秘SPFA——強大的單源最短路徑算法

探秘SPFA——強大的單源最短路徑算法

ron 直觀 rep 大於 pen body 操作 速度 並且

基於上次發blog,有位朋友讓我多寫些基本概念,就利用這次詳解偉大的SPFA算法來談。以下是百科上的算法簡介,很清楚,看一遍再繼續對理解程序很有幫助!(當然後面我也會解釋)

SPFA(Shortest Path Faster Algorithm)(隊列優化)算法是求單源最短路徑的一種算法,它還有一個重要的功能是判負環(在差分約束系統中會得以體現),在Bellman-ford算法的基礎上加上一個隊列優化,減少了冗余的松弛操作,是一種高效的最短路算法。

求單源最短路的SPFA算法的全稱是:Shortest Path Faster Algorithm,是西南交通大學段凡丁於1994年發表的(中國人的算法就是牛)。從名字我們就可以看出,這種算法在效率上一定有過人之處。很多時候,給定的圖存在負權邊,這時類似Dijkstra算法等便沒有了用武之地,而Bellman-Ford算法的復雜度又過高,SPFA算法便派上用場了。簡潔起見,我們約定加權有向圖G不存在負權回路,即最短路徑一定存在。如果某個點進入隊列的次數超過N次則存在負環(SPFA無法處理帶負環的圖)。當然,我們可以在執行該算法前做一次拓撲排序,以判斷是否存在負權回路,但這不是我們討論的重點。我們用數組d記錄每個結點的最短路徑估計值,而且用鄰接表來存儲圖G。我們采取的方法是動態逼近法:設立一個先進先出的隊列用來保存待優化的結點,優化時每次取出隊首結點u,並且用u點當前的最短路徑估計值對離開u點所指向的結點v進行松弛操作,如果v點的最短路徑估計值有所調整,且v點不在當前的隊列中,就將v點放入隊尾。這樣不斷從隊列中取出結點來進行松弛操作,直至隊列空為止。

定理:只要最短路徑存在,上述SPFA算法必定能求出最小值。證明:每次將點放入隊尾,都是經過松弛操作達到的。換言之,每次的優化將會有某個點v的最短路徑估計值d[v]變小。所以算法的執行會使d越來越小。由於我們假定圖中不存在負權回路,所以每個結點都有最短路徑值。因此,算法不會無限執行下去,隨著d值的逐漸變小,直到到達最短路徑值時,算法結束,這時的最短路徑估計值就是對應結點的最短路徑值。 期望時間復雜度:O(me), 其中m為所有頂點進隊的平均次數,可以證明m一般小於等於2n:“算法編程後實際運算情況表明m一般沒有超過2n.事實上頂點入隊次數m是一個不容易事先分析出來的數,但它確是一個隨圖的不同而略有不同的常數.所謂常數,就是與e無關,與n也無關,僅與邊的權值分布有關.一旦圖確定,權值確定,原點確定,m就是一個確定的常數.所以SPFA算法復雜度為O(e).證畢."(SPFA的論文)不過,這個證明是非常不嚴謹甚至錯誤的,事實上在bellman算法的論文中已有這方面的內容,所以國際上一般不承認SPFA算法。
對SPFA的一個很直觀的理解就是由無權圖的BFS轉化而來。在無權圖中,BFS首先到達的頂點所經歷的路徑一定是最短路(也就是經過的最少頂點數),所以此時利用數組記錄節點訪問可以使每個頂點只進隊一次,但在帶權圖中,最先到達的頂點所計算出來的路徑不一定是最短路。一個解決方法是放棄數組,此時所需時間自然就是指數級的,所以我們不能放棄數組,而是在處理一個已經在隊列中且當前所得的路徑比原來更好的頂點時,直接更新最優解。 SPFA算法有兩個優化策略SLF和LLL——SLF:Small Label First 策略,設要加入的節點是j,隊首元素為i,若dist(j)<dist(i),則將j插入隊首,否則插入隊尾; LLL:Large Label Last 策略,設隊首元素為i,隊列中所有dist值的平均值為x,若dist(i)>x則將i插入到隊尾,查找下一元素,直到找到某一i使得dist(i)<=x,則將i出隊進行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高約 50%。 在實際的應用中SPFA的算法時間效率不是很穩定,為了避免最壞情況的出現,通常使用效率更加穩定的Dijkstra算法。

順便解釋一下“松弛”:松弛操作是指對於每個頂點v∈V,都設置一個屬性d[v],用來描述從源點s到v的最短路徑上權值的上界,稱為最短路徑估計(shortest-path estimate)。 ————摘自《百度百科》

它的定義在上面第一句話解釋的不能再簡潔了,理解上述就好了。

至少我認為,SPFA算法是所有單源最短路算法中最實用的一種。(大佬們可以有其他想法,在此僅表示本人觀點)

還是用一道求最短路徑的模板題來解釋:

題目描述

【題意】 給出一個圖,起始點是1,結束點是N,邊是雙向的。求點1到點N的最短距離。哈哈,這就是標準的最短路徑問題。 【輸入格式】 第一行為兩個整數N(1≤N≤10000)和M(0≤M≤200000)。N表示圖中點的數目,M表示圖中邊的數目。 下來M行,每行三個整數x,y,c表示點x到點y之間存在一條邊長度為c。(x≠y,1≤c≤10000) 【輸出格式】 輸出一行,一個整數,即為點1到點N的最短距離。 如果點1和點N不聯通則輸出-1。 【樣例1輸入】 2 1 1 2 3 【樣例1輸出】 3
【樣例2輸入】 3 3 1 2 5 2 3 5 3 1 2 【樣例2輸出】 2
【樣例3輸入】 6 9 1 2 7 1 3 9 1 5 14 2 3 10 2 4 15 3 4 11 3 5 2 4 6 6 5 6 9 【樣例3輸出】 20 【數據規模】 30%:1<=n<=100 50%:1<=n<=1000 100%:1<=n<=10000
故事:如何所有點(包括終點)到出發點的距離最短(最近)。 1、給出一個圖有N個點,和一些有向邊(無向邊也行,多建立一個反向邊就是) 2、一開始出發點到出發點的距離為0,其它點到出發點的距離為無窮大。 3、核心思路:其他點都在迫切的想知道自己到出發點的距離,並且他們都想自己的好朋友能更近一點到出發點(更新自己的好朋友到出發點的距離) 4、核心思路:一個點什麽時候能更新自己好朋友到出發點的距離呢?當自己到出發點的距離變得更短的時候。 5、核心思路:我們建一個隊列q,讓能更新別人的點站到q裏面。然後讓q中的點一個一個出來更新。 6、最後沒人出來更新了,就結束,表示所有點到出發點的距離都是最短了。

我用的是模擬鏈表存圖,你用其他的鄰接矩陣也可以(要看點的規模,一般太大用鄰接矩陣很劃不來)

來吧,不廢話,上代碼:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 10005, M = 200005, oo = 0x3fffffff; //N表示最大點的個數 ,M表示最大邊的個數 ,oo是無窮大
struct Edge{
int to,wei,next; //鄰接鏈表的套路,to鄰接頂點,wei表邊的權重,next表鏈表指針
};
Edge edge[M]; //儲存邊的信息
int n,m,source,head[N],x,y,c,en(0),dist[N]; //dist【i】表示起點到 i的最短距離
bool inq[N];
queue<int> q;

void Addedge(int x,int y,int c) //存圖 ,x到y有一條權重為c的邊
{
edge[en].to=y;
edge[en].wei=c;
edge[en].next=head[x];
head[x]=en++;

}

void init()
{
cin>>n>>m;
memset(head,-1,sizeof(head)); //清零,作為每個點dfs的終止標誌
fill(inq,inq+n+1,false); //一開始都不在隊列中
while (m--)
{
cin>>x>>y>>c;
Addedge(x,y,c);
Addedge(y,x,c);
}
fill(dist,dist+n+1,oo); //先初始化為無窮大
source=1; //起點

}

void spfa() //這是套路
{
q.push(source);
dist[source]=0;
inq[source]=true;
while (!q.empty())
{
int u=q.front();
q.pop();
inq[u]=false;
for (int p=head[u];p!=-1;p=edge[p].next) //遍歷整張圖
{
int v=edge[p].to;
if (dist[v]>dist[u]+edge[p].wei) //如果到v的距離大於到u再加上u到v的距離,就更新
{
dist[v]=dist[u]+edge[p].wei;
if (inq[v]!=true)
{
q.push(v);
inq[v]=true; //指標記,防止重復進入,否則隊列沒有意義
}
}
}

}

}

void output()
{
cout<<dist[n]<<endl; //輸出到終點的距離就行了,其實你想輸出到哪點的最短距離都可以
}

int main()
{
init();//輸入存圖
spfa();//算法核心
output();//輸出答案

return 0;
}

代碼很簡潔,希望大家理解!

了解SPFA是十分有用的,如果認為自己掌握的不錯,就可以去做[NOIP提高組2009]最優貿易。提示:正反兩遍SPFA。

題解附上,最好先不看:

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include<iostream> #include<cstdio> #include<queue> #include<cstring> using namespace std; const int N = 100005, M = 500005; struct Edge{ int to,next; }; Edge edge[4*M]; int n,m,en(0),price[N],head1[N],head2[N],buy[N],sell[N]; bool inq[N]; queue<int> q; void insert(int head[],int x,int y){ edge[en].to = y; edge[en].next = head[x]; head[x] = en++; } void init(){ //freopen("trade.in","r",stdin); scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) scanf("%d",&price[i]); memset(head1,-1,sizeof(head1)); memset(head2,-1,sizeof(head2)); for (int i=0,x,y,z;i<m;i++){ scanf("%d%d%d",&x,&y,&z); insert(head1,x,y); insert(head2,y,x); if (z==2){ insert(head1,y,x); insert(head2,x,y); } } } void spfa1(){ int u,v; memset(buy,0x3f,sizeof(buy)); memset(inq,0,sizeof(inq)); buy[1] = price[1]; q.push(1) ; inq[1] = true; while (!q.empty() ){ u = q.front() ; q.pop(); inq[u] = false; for (int p=head1[u]; p!=-1; p=edge[p].next ){ v = edge[p].to ; if (min(buy[u],price[v])<buy[v]){ buy[v] = min(buy[u],price[v]); if (!inq[v]){ q.push(v); inq[v] = true; } } } } } void spfa2(){ int u,v; memset(sell,0,sizeof(sell)); memset(inq,0,sizeof(inq)); sell[n] = price[n]; q.push(n); inq[n] = true; while (!q.empty() ){ u = q.front(); q.pop(); inq[u] = false; for (int p=head2[u]; p!=-1; p=edge[p].next ){ v = edge[p].to; if (max(sell[u],price[v])>sell[v]){ sell[v] = max(sell[u],price[v]); if (!inq[v]){ q.push(v) ; inq[v] = true; } } } } } int main(){ init(); spfa1(); spfa2(); int ans=0; for (int i=1;i<=n;i++) ans = max(ans,sell[i]-buy[i]); //freopen("trade.out","w",stdout); printf("%d\n",ans); return 0; }

探秘SPFA——強大的單源最短路徑算法