常用最短路算法——-SPFA和Dijkstra及其優化
這篇文章將簡單講解兩個最常用的最短路優化算法,需要讀者有一定的圖論基礎。
首先從DIJKSTRA講起。常規的dijkstra算法復雜度較高,為O(n^2),因為要花大量時間來找當前已知的距頂點距離最小的值,所以用優先隊列(值小的先出隊列)來優化,可大大優化時間復雜度。STL中優先隊列的操作單次復雜度為O(logN),所以經過優先隊列優化的dijkstra時間復雜度會降到O(N*logN);
以下為核心部分代碼:

1 struct pack{int s,dist;};//利用一個結構體存儲節點的起點與到達它的最小距離 2 bool operator<(const pack &a,const pack&b){ 3 return a.dist>b.dist; 4 }//重載pack的小於號,使得大根堆的優先隊列為小根堆 5 priority_queue<pack> q; 6 void dij(){ 7 memset(dist,0x7f,sizeof(dist));//初始化最大值 8 memset(vis,0,sizeof(vis)); 9 int s=1; 10 vis[s]=1; 11 dist[s]=0; 12 q.push((pack){s,dist[s]});//將起點放入 13 while(!q.empty()){ 14 pack t=q.top();q.pop();//取出距離最小的點 15 int from=t.s; 16 if(vis[from])continue; 17 vis[from]=1; 18 for(int i=first[from];i;i=edge[i].next){ 19 int to=edge[i].to 20 if(dist[to]>edge[i].val+t.dist){ 21 dist[to]=edge[i].val+t.dist; 22 q.push((pack){to,dist[to]});//松弛操作 23 } 24 } 25 } 26 }
很多時候,給定的圖存在負權邊,這時類似Dijkstra算法等便沒有了用武之地,而Bellman-Ford算法的復雜度又過高,SPFA算法便派上用場了。簡潔起見,我們約定加權有向圖G不存在負權回路,即最短路徑一定存在。如果某個點進入隊列的次數超過N次則存在負環(SPFA無法處理帶負環的圖)。
我們用數組dist記錄每個結點的最短路徑估計值,而且用鄰接表來存儲圖G。我們采取的方法是動態逼近法:設立一個先進先出的隊列用來保存待優化的結點,優化時每次取出隊首結點from,並且用from點當前的最短路徑估計值對離開from點所指向的結點to進行松弛操作,如果to點的最短路徑估計值有所調整,且to點不在當前的隊列中,就將to點放入隊尾。這樣不斷從隊列中取出結點來進行松弛操作,直至隊列空為止。
期望時間復雜度:O(mE), 其中m為所有頂點進隊的平均次數,可以證明m一般小於等於2n;(除非數據點專門卡SPFA,通常m都不大)
以下是隊列實現的SPFA模板

queue<int> q; void spfa(int s){ memset(vis,0,sizeof(vis)); memset(dist,0x7f,sizeof(dist)); vis[s]=1; dis[s]=0; q.push(start); while(q.size()){ int from=q.front(); for(int i=first[from];i;i=edge[i].next){ int to=edge[i].to; if(dist[from]+edge[i].val<dist[to]){ dist[to]=dist[from]+edge[i].val; if(!vis[to]){ q.push(to); vis[to]=1; } } } vis[from]=0; q.pop(); } }
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算法。(經過優化的DIJ復雜度穩定在O(N*logN)左右)
下面附利用STL中的雙端隊列優化的SLF優化代碼:

int spfa(int start){ memset(vis,0,sizeof(vis)); memset(dist,0x7f,sizeof(dist)); deque<int> q;//STL中的雙端隊列 可在兩端實現入隊出隊 vis[start]=1; dist[start]=0; num[start]++; q.push_back(start); while(q.size()){ int from= q.front();q.pop_front();//每次從前端取出 vis[from]=0; for(int i=first[from];i;i=edge[i].next){ int to=edge[i].to; if(dist[from]+edge[i].val<dist[to]){ dist[to]=dist[from]+edge[i].val; if(!vis[to]){ vis[to]=1; num[to]++; if(num[to]==n)return 0;//判斷是否有負環存在 if(!q.empty()){ if(dist[to]>dist[q.front()])//距離大的往后端放 q.push_back(to); else q.push_front(to);//小的放前端 } else q.push_back(to); } } } } return 1; }
例題 CodeVS 1021 瑪麗卡
題目描述 Description麥克找了個新女朋友,瑪麗卡對他非常惱火並伺機報復。
因為她和他們不住在同一個城市,因此她開始准備她的長途旅行。
在這個國家中每兩個城市之間最多只有一條路相通,並且我們知道從一個城市到另一個城市路上所需花費的時間。
麥克在車中無意中聽到有一條路正在維修,並且那兒正堵車,但沒聽清楚到底是哪一條路。無論哪一條路正在維修,從瑪麗卡所在的城市都能到達麥克所在的城市。
瑪麗卡將只從不堵車的路上通過,並且她將按最短路線行車。麥克希望知道在最糟糕的情況下瑪麗卡到達他所在的城市需要多長時間,這樣他就能保證他的女朋友離開該城市足夠遠。
編寫程序,幫助麥克找出瑪麗卡按最短路線通過不堵車道路到達他所在城市所需的最長時間(用分鍾表示)。
輸入描述 Input Description第一行有兩個用空格隔開的數N和M,分別表示城市的數量以及城市間道路的數量。1≤N≤1000,1≤M≤N*(N-1)/2。城市用數字1至N標識,麥克在城市1中,瑪麗卡在城市N中。
接下來的M行中每行包含三個用空格隔開的數A,B和V。其中1≤A,B≤N,1≤V≤1000。這些數字表示在A和城市B中間有一條雙行道,並且在V分鍾內是就能通過。
輸出描述 Output Description輸出文件的第一行中寫出用分鍾表示的最長時間,在這段時間中,無論哪條路在堵車,瑪麗卡應該能夠到達麥克處,如果少於這個時間的話,則必定存在一條路,該條路一旦堵車,瑪麗卡就不能夠趕到麥克處。
樣例輸入 Sample Input5 7
1 2 8
1 4 10
2 3 9
2 4 10
2 5 1
3 4 7
3 5 10
樣例輸出 Sample Output27
先DIJ記錄最短路徑的邊 然后枚舉去掉每一條最短路徑的邊,DIJ最后取最大值
當然,可以將DIJ換成SPFA,也是同樣能AC的
DIJ AC:
#include<bits/stdc++.h> using namespace std; int n,m,cnt,knt; int vis[1010],dist[1010],first[1010],next[600000]; bool ask[1010][1010]; int read(int &x){ //讀入優化 char c=getchar();x=0; while(c<'0'||c>'9')c=getchar(); while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar(); return x; } struct EDGE{ int from,to,val,next; }edge[600000]; struct pack{ int s,dist; }; bool operator<(const pack &a,const pack &b){ return a.dist>b.dist; } void addedge(int a,int b,int c){ edge[++cnt].next=first[a]; edge[cnt].from=a; edge[cnt].to=b; edge[cnt].val=c; first[a]=cnt; } void ini(){ read(n);read(m); int x,y,z; for(int i=1;i<=m;i++){ read(x);read(y);read(z); addedge(x,y,z); addedge(y,x,z); } } void dijclear(){ memset(dist,0x7f,sizeof(dist)); memset(vis,0,sizeof(vis)); } int dij(int s){ dijclear(); priority_queue<pack> q; q.push((pack){s,dist[s]=0}); while(!q.empty()){ pack temp=q.top();q.pop(); int from=temp.s; if(vis[from])continue; vis[from]=1; int k; for(int i=first[from];i;i=edge[i].next){ if(temp.dist+edge[i].val<dist[edge[i].to]){ dist[edge[i].to]=temp.dist+edge[i].val; next[edge[i].to]=from; q.push((pack){edge[i].to,dist[edge[i].to]}); } } } return dist[1]; } int dij2(int s){ dijclear(); priority_queue<pack> q; q.push((pack){s,dist[s]=0}); while(!q.empty()){ pack temp=q.top();q.pop(); int from=temp.s; if(vis[from])continue; vis[from]=1; int k; for(int i=first[from];i;i=edge[i].next){ if(temp.dist+edge[i].val<dist[edge[i].to]&&!ask[from][edge[i].to]){ dist[edge[i].to]=temp.dist+edge[i].val; q.push((pack){edge[i].to,dist[edge[i].to]}); } } } return dist[1]; } int main(){ ini(); int ans=dij(n); for(int i=1;i!=n;i=next[i]){ ask[next[i]][i]=1; ans=max(dij2(n),ans);//將每條邊遮住一遍多次求最短路 ask[next[i]][i]=0; } printf("%d",ans); /*int x=1; while(x!=0){ printf("%d->",x); x=next[x]; } */ return 0; }