1. 程式人生 > >離散數學課本上的最短路徑演算法

離散數學課本上的最短路徑演算法

Dijkstra演算法是一個經典的演算法——他是荷蘭電腦科學家Dijkstra於1959年提出的單源圖最短路徑演算法,也是一個經典的貪心演算法。所謂單源圖 是規定一個起點的圖,我們的最短路徑都是從這個起點出發計算的。演算法的適用範圍是一個無向(或者有向圖),所有邊權都是非負數。
演算法描述:

節點集合V = {}空集合,距離初始化。
節點編號0..n – 1, 起點編號0≤ s < n。
距離陣列
起點 d[s] = 0
其他 d[i] = ∞, 0 ≤ i < n,  i ≠ s。
迴圈n次

找到節點i 不屬於 V,且d[i]值最小的節點i。

V = V + i

對所有滿足j  V的邊(i, j) 更新d[j] = min(d[j] , d[i] + w(i,  j))。
以下圖為例,描述Dijkstra演算法的執行過程:

初始,求A點到其他點的最短路徑(也稱單源最短路徑)。
初始化A點
A點有3條邊,AB(17),AE(16),AF(1)。

將3條邊加入優先佇列,此時佇列中的元素為(只記錄目標點):

{1 F} | {16 E} | {17 B} 取出佇列中最小的元素,{1 F},F點是一個未處理過的點,因此得到了A點到F點的最短距離。更新距離,變為:
處理F點,F點有4條邊。FA(1),FB(11),FD(14),FE(33)。其中FA已經處理過,所以忽略掉。
將3條邊加入優先佇列,注意,此時加入佇列時,所有邊的權值需要加上F點到A點的最短距離1。此時佇列中的元素為:

{12 B} | {15 D}  | {16 E} | {17 B} | {34 E} 取出佇列中最小的元素,{12 B},B點是一個未處理過的點,因此得到了A點到B點的最短距離。更新距離,變為:
處理B點,B點有4條邊。AB(17),BF(11),BC(6),BD(5)。其中AB,BF已經處理過,所以忽略掉。

將2條的權值加上A到B的最短路徑12,加入優先佇列。此時佇列中的元素為:


{15 D}  | {16 E} | {17 B} | {17 D} | {18 C} | {34 E}


取出佇列中最小的元素,{15 D},D點是一個未處理過的點,因此得到了A點到D點的最短距離。更新距離,變為:


處理D點,D點有4條邊。其中DC(10),DE(4)沒有處理過。

將2條的權值加上A到D的最短路徑15,加入優先佇列。此時佇列中的元素為:

{16 E} | {17 B} | {17 D} | {18 C} | {19 E} | {25 C} | {34 E}



取出佇列中最小的元素,{16 E},E點是一個未處理過的點,因此得到了A點到E點的最短距離。更新距離,變為:

處理E點,E點所連線的邊都已經被處理過了。
此時優先佇列中的元素為:
{17 B} | {17 D} | {18 C} | {19 E} | {25 C} | {34 E}

取出佇列中最小的元素,{17 B},B點是一個已經處理過的點,因此繼續後面的處理。  {17 D} | {18 C} | {19 E} | {25 C} | {34 E}
取出佇列中最小的元素,{17 D},D點是一個已經處理過的點,因此繼續後面的處理。
 {18 C} | {19 E} | {25 C} | {34 E}

取出佇列中最小的元素,{18 C},C點是一個未處理過的點,因此得到了A點到C點的最短距離。更新距離,變為:

Dijkstra演算法的證明:

i  V,  d[i] = min{d[x] + w(x, i), x  V}

我們證明節點i要進入集合V時,d[i]確實是s到i的最短路長度 。
歸納證明: 起初 d[s] = 0滿足條件。 假設之前集合V中的點全部滿足假設,現在要加入節點i   V,假設任意從s到i的路徑P= s…x y…i。
其中s..x全部在V中, y  V。根據歸納假設d[x]是s到x的最短路長度。 根據d的定義,我們有d[x] + w(x,y) ≥ d[y]。
而且因為dijkstra選擇最小的d加入,所以有d[y] ≥ d[i] 。
於是有路徑P的長度, length(P) ≥  d[x] + w(x, y) + length(y..i) ≥ d[y] + length(y..i)  ≥  d[y] ≥ d[i]。
從而d[i]也是最短路的長度。得證。
例題:

你來到一個迷宮前。該迷宮由若干個房間組成,每個房間都有一個得分,第一次進入這個房間,你就可以得到這個分數。還有若干雙向道路連結這些房間,你沿著這些道路從一個房間走到另外一個房間需要一些時間。遊戲規定了你的起點和終點房間,你首要目標是從起點儘快到達終點,在滿足首要目標的前提下,使得你的得分總和儘可能大。現在問題來了,給定房間、道路、分數、起點和終點等全部資訊,你能計算在儘快離開迷宮的前提下,你的最大得分是多少麼?

最後,我們來提供輸入輸出資料,由你來寫一段程式,實現這個演算法,只有寫出了正確的程式,才能繼續後面的課程。

輸入
第一行4個整數n (<=500), m, start, end。n表示房間的個數,房間編號從0到(n - 1),m表示道路數,任意兩個房間之間最多隻有一條道路,start和end表示起點和終點房間的編號。
第二行包含n個空格分隔的正整數(不超過600),表示進入每個房間你的得分。
再接下來m行,每行3個空格分隔的整數x, y, z (0<z<=200)表示道路,表示從房間x到房間y(雙向)的道路,注意,最多隻有一條道路連結兩個房間, 你需要的時間為z。
輸入保證從start到end至少有一條路徑。

輸出
一行,兩個空格分隔的整數,第一個表示你最少需要的時間,第二個表示你在最少時間前提下可以獲得的最大得分。
輸入示例
3 2 0 2
1 2 3
0 1 10
1 2 11

輸出示例
21 6

程式碼:

#include<iostream>
#include<cmath>
using namespace std;
int map[505][505];
int vis[505],val[505],dis[505],sum[505];
const int inf=999999;
int main()
{
 int n,m,s,e;
 cin>>n>>m>>s>>e;
 int i,j;
 for(i=0;i<n;i++)
 cin>>val[i];
 for(i=0;i<n;i++)
 for(j=0;j<n;j++)
 {
  if(i==j)
  map[i][j]=0;
  else
  map[i][j]=inf;
 }
 int x,y,z;
 for(i=0;i<m;i++)
 {
  cin>>x>>y>>z;
  map[x][y]=z;
  map[y][x]=z;
 }
 for(i=0;i<n;i++)
 {
  vis[i]=0;
  dis[i]=inf;
  sum[i]=0;
 }
 dis[s]=0;
 sum[s]=val[s];
 int min,v;
 for(i=0;i<n;i++)
 {
  min=inf;
  v=0;
  for(j=0;j<n;j++)
  {
   if(vis[j]==0)
   {
    if(min>dis[j])
    {
     min=dis[j];
     v=j;
    }
   }
  }
  vis[v]=1;
  for(j=0;j<n;j++)
  {
   if(vis[j]==0)
   {
    if(dis[j]>dis[v]+map[v][j])
    {
     dis[j]=dis[v]+map[v][j];
     sum[j]=sum[v]+val[j];
    }
    else if(dis[j]==dis[v]+map[v][j])
    sum[j]=max(sum[v]+val[j],sum[j]);
   }
  }
 }
 cout<<dis[e]<<" "<<sum[e]<<endl;
 return 0;
}