圖論(三) (一)最短路徑算法 2.Dijkstra算法
Dijkstra 算法解決的是帶權重的有向圖上單源最短路徑問題,該算法要求所有邊的權重都為非負值。該算法的時間復雜度是O(N2),相比於處理無負權的圖時,比Bellmad-Ford算法效率更高。
算法描述:
首先引用《算法導論》中的一段比較官方的話,如果可以看懂,那下一部分就可以跳過了:
“Dijkstra算法在運行過程中維持的關鍵信息是一組結點集合S。從源結點s到該集合中每個結點之間的最短路徑已經被找到。算法重復從結點集 V - S 中算則最短路徑估計的最小的結點 u ,將 u 加入到集合S,然後對所有從 u 出發的邊進行松弛。” 所謂松弛操作,簡單的說就是更新兩點間的最短距離。
不是很好理解對吧,那麽下面的描述是更容易理解的一種描述:
設起始點為s,dis[v]表示s點到v點的最短路徑,pre[v]是v的前驅結點,用來輸出路徑。
1、初始化:dis[v]=∞(v≠s) dis[s]=0,pre[s]=0;
2、for(i=1;i<=n;i++)
(1)在沒有被訪問過的點中,即上述的V - S集合,找到一個點 u 使得dis[u]是最小的。
(2)標記 u 為已確定的最短路徑。
(3)for(每個與 u 相連且沒有確定過最短距離的點 v)
if(dis[u]+m[u][v]<dis[v]){
dis[v]=dis[u]+m[u][v];
pre[v]=u;
}
3、結束:結果dis[v]就是s到v的最短距離。
算法理解:
其中自以為有幾點理解需要說明:
1、為什麽用到中間點?
2、取s到中間點的距離時采用什麽策略?
第一個問題,從起點到另一個點的最短路徑至少會經歷一個中間點,所以我們要求出經過這個中間的到另一個點的路徑,就要先求出起點到中間點的最短路徑。
第二個問題,其實這裏采用的是一中貪心的策略。當然這個策略可以被嚴格證明是正確的,但是我也一知半解,只知道是可以被證明的,在這裏也就不浪費時間了。(詳細可以參考《算法導論》)
最後,解釋一下為什麽有負權邊的時候不可以:
連接矩陣如下(圖可以自己在旁邊畫一下):
1 2 3
1 \ 2 1
2 2 \ -4
3 1 -4 \
那麽第一次標記的點就為3並且把dis[3]記為1,但實際上dis[3]應該時-2,因此就會出現錯誤。
最後附上一段不那麽標準的代碼:
1 #include<stdio.h> 2 #include<stdlib.h> 3 int m[100][100],e,dist[100],n,b[100],pre[100],dist[100]; 4 void dij(int s){ 5 b[s]=1; 6 int i,j; 7 for(i=1;i<=n;i++) 8 dist[i]=m[s][i]; 9 dist[s]=0; 10 pre[s]=0; 11 12 for(i=1;i<=n;i++){ 13 int min=1000000,k=0; 14 for(j=1;j<=n;j++) 15 if(b[j]!=1 && dist[j]<min) 16 {min=dist[j];k=j;} 17 b[k]=1; 18 for(j=1;j<=n;j++) 19 if(min+m[k][j]<dist[j]&&b[j]!=1) 20 { 21 dist[j]=min+m[k][j]; 22 pre[j]=i; 23 } 24 } 25 for(i=1;i<=n;i++) 26 if(i!=s) 27 printf("%d ",dist[i]); 28 } 29 int main(){ 30 int i,j; 31 scanf("%d%d",&n,&e); 32 memset(b,0,sizeof(b)); 33 memset(m,10000,sizeof(m)); 34 for(i=1;i<=e;i++){ 35 int x,y; 36 scanf("%d%d",&x,&y); 37 scanf("%d",&m[x][y]); 38 } 39 int w; 40 scanf("%d",&w); 41 dij(w); 42 system("pause"); 43 return 0; 44 }
圖論(三) (一)最短路徑算法 2.Dijkstra算法