最短路徑演算法(Dijkstra)
一、前言
最短路徑演算法,顧名思義就是求解某點到某點的最短的距離、消耗、費用等等,有各種各樣的描述,在地圖上看,可以說是圖上一個地點到達另外一個地點的最短的距離。比方說,我們把地圖上的每一個城市想象成一個點,從一個城市到另一個城市的花費是不一樣的。現在我們要從上海去往北京,需要考慮的是找到一條路線,使得從上海到北京的花費最小。有人可能首先會想到,飛機直達啊,這當然是時間消耗最小的方法,但是考慮到費用的高昂,這條線路甚至還不如上海到北京的高鐵可取。更有甚者,假設國家開通了從上海到西藏,再從西藏到蘭州等等城市經過萬般周折最後到達北京的一條線路,雖然要需要經歷較長一段時間,但是價錢相比前二者非常實惠(假設只要一塊錢,便能跑大半個中國,領略多省風光),單從省錢的角度看來,自然最後這條是可取的。這就是我們在這裡所說的單源最短路徑。我們接下來的篇幅中將去講解所有邊權值為非負的有向圖的單源最短路徑,由於無向圖相當於變相的有向圖,在這裡就不做解釋,留作讀者自行推廣。
二、概念
這裡我們講解最短路徑,需要掌握幾個基本的概念:
對於有向圖G=(V,E),權值函式W: E→R(即每條邊的權值都為一個實數)
1、路徑
表示從v1到vk的一條路徑,它的權值為:
2、最短路徑:從u到v的一條路徑,使w(p)最小,w(p)。
3、最短路徑權值:
注意,最短路徑可能不存在:
(1)存在負權迴路,例如:
可以看出,存在v1到v6的負權迴路,它的權值為-3,如果我們想找從u到v的最短路徑,那麼無限迴圈地走這個負權迴路可以使最短路徑越來越小,最後達到負無窮, 那麼就說明找不到從u到v的最短路徑。
(2)不存在從u到v的路徑,這個是肯定不會存在最短路徑的。
三、最優子結構
我們不難發現,求解源點到某一頂點的最短路徑,其實不比求解源點到所有頂點的路徑簡單。這個時候我們要引入全域性的概念,能不能找出所有的頂點的最短路徑,然後再去檢視到目標點的最短路徑呢?很多人就會想到動態規劃這一思想,說道動態規劃,自然我們首先要考慮的問題是最優子結構。
最短路徑滿足最優子結構性質:最短路徑的子路徑是最短路徑。
前提:u到v是最短路徑。
假設:x到y不是最短路徑,那麼存在一條更短的路徑從x到y(假設為下面的彎箭頭),這樣,刪去原路徑中從x到y的路徑,用新找到的路徑替代(彎箭頭),那麼就得到 了一條比u到v的權值更短的路徑,這與前提u到v是最短路徑相矛盾,因而x到y是最短路徑,即最短路徑滿足最優子結構性質。
引入三角不等式的概念:(從u到v的最短路徑權值,小於等於從u到x的最短路徑權值加上從x到v的最短路徑權值)
四、單元最短路徑問題
對於圖G= (V, E),給定源點s,找到從s到所有頂點v的最短路徑由於在本文中我們講解Dijkstra演算法,需要假設沒有負權值,即: 因而,只要路徑存在,便存在最短路徑。
五、Dijkstra演算法思想
Dijkstra是非常非常著名的電腦科學家,可能很多人對他的瞭解只在他的單元最短路徑演算法上,對作業系統瞭解的人可能還了解他的銀行家演算法,在方法學領域還有goto有害論等等。當然要講他的其他傑出貢獻,那就要把話題扯遠了,今天我們只講講Dijkstra演算法的思想,然後在後面給出它的實現過程,必要的給出其正確性的證明(Dijkstra在程式正確性證明領域發明了最弱前置謂詞證明方法,不過我們不會用他的方法證明他老人家的程式的正確性,不然就扯到了十萬八千里-。-)。
演算法思想:
(1)在任意時刻,我們都要得到從源點到所有頂點的估算距離,並維持一個頂點集合S,若頂點v在S中,則說明從源點到v的最短路徑已知;
(2)在每一次將不在S中的頂點v加到S中去時,總是選擇從源點到v的估算距離最小的;
(3)頂點v加入S中之後,對於所有與v相鄰的頂點(不屬於S),更新它們的估算距離。
由(2),我們看到了貪心的影子,在每次選擇時,我們總是想選擇花費最小的,正常人都會這樣去想,至於為什麼這樣選,這樣選對不對,我們將在後面進行證明。
虛擬碼如下:
Dijkstra(G, W, s) //G表示圖,W表示權值函式,s表示源頂點
d[s] ←0 //源點到源點最短路為0
for each v ∈ V - {s} //3-8行均為初始化操作
do d[v]←∞
parent[v]←NIL
S←∅
Q←V //此處Q為優先佇列,儲存未進入S的各頂點以及從源點到這些頂點的估算距離,採用二叉堆(最小堆)實現,越小越優先
while Q≠∅
do u←Extract-Min(Q) //提取估算距離最小的頂點,在優先佇列中位於頂部,出佇列,放入集合S中
S←S∪{u}
for each v ∈ Adj(u) //鬆弛操作,對與u相鄰的每個頂點v,進行維持三角不等式成立的鬆弛操作。
do if d[v] > d[u] + w(u, v)
then d[v] = d[u] + w(u, v) //這一步隱含了更新優先佇列中的值。
parent[v]←u //置v的前驅結點為u
六、簡單例子說明
初始情況:
第一次鬆弛,選取A頂點:
第二次鬆弛,C的估算距離最小,選取C頂點:
第三次鬆弛,E的估算距離最小,選取E:
第四次鬆弛,B的估算距離最小,選取B:
第五次鬆弛:(最後一個點,完成)
經過所有的鬆弛操作之後,我們就得到了所有頂點的最短路徑(表格中紅字部分)。
如果加上對parent[]進行的操作,我們還可以得到一棵最短路徑樹,這個讀者可以自行推廣。
七、程式碼實現
#include <iostream>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#define INF 0x3f3f3f3f
using namespace std;
const int MAX=2500;
int dis[MAX];
int n,m;//節點數,與邊數
struct node
{
int t,v;
};
struct ke
{
int me,w;
friend bool operator<(ke n1,ke n2)
{
return n1.w>n2.w;
}
};
vector<node>edge[MAX];
void dijkstra(int nn)
{
ke a;
memset(dis,INF,sizeof(dis));
priority_queue<ke>Q;
dis[nn]=0;
a.me=nn,a.w=0;
Q.push(a);
while(!Q.empty())
{
ke p=Q.top();
Q.pop();
for(int i=0;i<edge[p.me].size();++i)
{
int to=edge[p.me][i].t;
int v=edge[p.me][i].v;
if(dis[to]>dis[p.me]+v)
{
dis[to]=dis[p.me]+v;
ke a;
a.me=to,a.w=dis[to];
Q.push(a);
}
}
}
}
int main()
{
while(scanf("%d %d",&m,&n)!=EOF)
{
int fo,to,v;
node a;
for(int i=0;i<MAX;++i)
edge[i].clear();
for(int i=0;i<m;++i)
{
scanf("%d %d %d",&fo,&to,&v);
a.t=to,a.v=v;
edge[fo].push_back(a);
a.t=fo,a.v=v;
edge[to].push_back(a);
}
dijkstra(1);
// for(int i=1;i<=n;++i)
// printf("%-3d",dis[i]);
printf("%d\n",dis[n]);
}
return 0;
}