演算法導論之單源最短路徑
單源最短路徑,在現實中是很多應用的,是圖的經典應用,比如在地圖中找出兩個點之間的最短距離、最小運費等。單源最短路徑的問題:已知圖G=(V,E),找出給定源頂點s∈V到每個頂點v∈V的最短路徑。單源最短路徑衍生出的變體問題如下:
1)單終點最短路徑問題:找出從每個頂點v到指定終點t的最短路徑。這個是單源最短路徑的反向,把圖的每條邊反向,問題就變成單源最短路徑的問題;
2)單對頂點最短路徑問題:對於給定頂點u和v,找出從u到v的一條最短路徑。找出所有頂點的單源最短路徑,該問題自然得解。
3)每隊頂點間最短路徑問題:對於給定頂點u和v,找出從u到v的最短路徑。單獨討論
針對單源最短路徑問題,構建如下的數學模型來描述:
給定的帶權有向圖G=(V,E),加權函式w:E->R為從邊到實型權值的對映。路徑p=<v0,v1,…,vk>的權是指其組成邊的所有權值之和:
如果u到v存在一條路徑,則其最小權值的邊集合為最短路徑。
最短路徑演算法具有動態規劃和貪心演算法的最優子結構性質:最短路徑的子路徑是最短路徑,即一條頂點間的最短路徑包含路徑上其他的最短路徑。
對圖求解最短路徑的問題,存在負權值邊和負權迴路的情況。經過負權值邊,權值會減小,而無限次經過負權迴路,權值會趨近-∞,所以一條最短路徑是不應該包含負權迴路。在最短路徑演算法總,Dijkstra演算法假定圖的所有邊權值都非負;而Bellman-Ford演算法,允許圖存在負權邊,但不能存在從源點可達的負權迴路,該演算法還能檢測出負權迴路。
最短路徑之於一個圖的發現,其實是一棵有根的最短路徑樹,只要回溯每個頂點的父頂點即可。定義定點集Vπ為G總所有具有非空前趨(存在負頂點)的頂點集合,再加上源點s。
Vπ={v∈V:π[v]≠null}U{s}
有向邊集Eπ是有Vπ中的頂點π值匯出的邊集:
Eπ={(π[v],v)∈E:v∈Vπ-{s}}
Gπ(Vπ,,Eπ)就是最短路徑樹,s是根節點。
設圖G=(V,E)是帶權有向圖,其加權函式w:E->R,並假定G中不包含從源點s∈V可達的權值為負的迴路,那麼最短路徑是良定義 。以s為根的最短路徑樹是有向子圖Gπ(Vπ,,Eπ),其中Vπ⊆V,Eπ⊆E,那麼:
1)Vπ是G中從s可達的頂點集合;
2)Gπ形成一顆以s為根的有根樹;
3)對所有v屬於Vπ,Gπ中從s到v的唯一簡單路徑是G中從s到v的最短路徑。
當然最短路徑並不是唯一的,最短路徑樹也是。
導論中給出鬆弛技術,就是表示緊縮上界的方法。對每個頂點v∈V,設定一個屬性d[v],表示從源點s到v的最短路徑上權值的上界,稱為最短路徑估計。鬆弛操作,對每個頂點d[v]初始為∞,鬆弛一條邊(u,v)可以減小最短路徑估計的值d[v],並更新v的前趨π[v]。
Relax_Func(){
if d[v]>d[u]+w(u,v)
then d[v]=d[u]+w(u,v)
π[v]=u
}
鬆弛操作,通俗地說,就是先設定d[v]最大,然後找出其邊權來更新d[v]和π[v],從而不斷減小d[v]值,是演算法的一種指示變數。
最短路徑和鬆弛操作是最短路徑演算法的基礎,具有三角不等式、上界、無路徑、收斂、路徑鬆弛、前趨子圖六個性質。還有一個無窮運算的約定:
假定對任何實數a≠-∞,有a+∞=∞+a=∞,出現負權迴路下,假定對任何實數a≠∞,有a+(-∞)= (-∞)+a=-∞。
1)Bellman-Ford演算法
Bellman-Ford演算法支援存在負權邊的情況下,解決單源最短路徑問題。輸入給定的帶權有向圖G=(V,E),其源點為s,加權函式為w:E->R,執行Bellman-Ford演算法後輸出一個返回值,表明圖中是否存在著一個從源點可達的權為負的迴路。如存在,則無解,否則將產生最短路徑及其權值。
簡單來說,Bellman-Ford演算法的執行結果將確認是否存在負權迴路,如果沒有就會產生最短路徑及其權值。具體演算法如下:
Func_Bellman-Ford(G,w,s){
Func_Initialize-single-source(G,s);//初始化每個頂點的d和π值
//開始進行鬆弛操作
for i=1 to |V(G)|-1
do for each edge (u,v)∈E(G)
do Func_Relax(u,v,w)
for each edge(u,v) ∈E(G)
do if d[v]>d[u]+w(u,v) //已經鬆弛操作了,如果還存在子頂點大於負頂點權值的情況,則存在負權迴路
then return false //存在負權迴路,返回false
return true
}
Func_Relax(u,v,w){
if d[v]>d[u]+w(u,v)
then d[v]= d[u]+w(u,v)
π[v]=u
}
Func_Initialize-single-source(G,s){
for eachvertex v∈V[G]
do d[v]=∞
π[v]=null
d[s]=0
}
Bellman-Ford演算法的執行時間為O(VE),主要是如何證明該演算法執行結果的正確性。演算法導論中通過反證法和三角不等式證明了該演算法可以正確計算出從源點出到所有頂點的最小路徑的權。
有興趣可以理解下證明過程,不過中心思想依然是說明不存在負權迴路情況下從源點到所有頂點間的最小路徑權值。
在有向無迴路情況下,按頂點的拓撲序列對某加權dag圖(有向無迴路圖)G=(V,E)的邊進行鬆弛後,可以在⊙(V+E)時間內計算出單源最短路徑。Dag圖最短路徑總是存在的,即使圖中有權值為負的邊存在,也不可能存在負權迴路。
有向無迴路圖中的單源最短路徑演算法開始對dag圖進行拓撲排序(運用DFS),獲得頂點的線性序列。如果從頂點u到v存在一條路徑,則在拓撲序列中u先於v;對頂點線性序列的每個頂點做鬆弛操作即獲得單源最短路徑。演算法的典型應用是在PERT圖分析中確定關鍵路徑。關鍵路徑是通過dag的一條最長路徑,對應於執行一個有序的工作序列的最長時間。關鍵路徑的權值是完成所有工作所需要時間的下限。理解下就是這個演算法適合於序列圖。
2)Dijkstra演算法
Dijkstra演算法解決有向圖G=(V,E)上帶權的單源最短路徑問題,但要求所有邊的權值非負,即每條邊(u,v)∈E,有w(u,v)≥0。Dijkstra演算法有一個頂點集合S,包含具有最短路徑的頂點。演算法反覆選擇具有最短路徑估計的頂點u∈V-S,將u加入S,對u的所有邊進行鬆弛操作。演算法採用了最小優先佇列Q來排序關鍵字為頂點的d值。最小優先佇列操作有插入、抽取最小、刪除三個操作,不同資料結構實現由不同的時間效能:第一種陣列結構,從1到|V|編好號的頂點,簡單將d[v]存入一個數組的第v項,插入和刪除操作都是O(1),但搜尋最小時間是O(V);第二種二叉最小堆結構,時間效能有提升;第三種是斐波那契堆,時間效能也會有提升。Dijkstra演算法和最小生成樹的Prim演算法相似。下面看具體演算法:
Func_Relax(u,v,w){
if d[v]>d[u]+w(u,v)
then d[v]= d[u]+w(u,v)
π[v]=u
}
Func_Initialize-single-source(G,s){
for eachvertex v∈V[G]
do d[v]=∞
π[v]=null
d[s]=0
}
Func_Dijkstra(G,w,s){
Func_Initialize-single-source(G,s)
S=∅
Q=V[G] //包含所有頂點,最小優先佇列插入
While Q≠∅
do u=Extract-Min(Q) //最小優先佇列搜尋最小值
S=SU{u}
for each vertex v∈Adj[u]//所有u的鄰接表中的頂點
do Func_Relax(u,v,w) //包含最小優先佇列u出列
}
Dijkstra演算法總是在V-S中選擇最輕或最近的頂點插入集合S中,是一種貪心策略。演算法導論中證明了Dijkstra演算法應用貪心策略可以得出最短路徑。證明過程採用的數學思路也很值得去細細品味,包括最短路徑的性質的證明:三角不等式、上界性質、無路徑性質、收斂性質、路徑鬆弛性質和前驅子圖性質(構建以s為根的最短路徑樹)。在演算法中,樹和圖是有著緊密聯絡。
3)差分約束系統與最短路徑
將計算機中的某類問題求解轉化成數學模型來求解,是一以貫之的。同樣的,將不包含負權迴路的圖G=(V,E)求解單源最短路徑問題轉為線性規劃中的差分約束系統模型來求解,而Bellman-Ford演算法實際就是一種線性規劃演算法。
一般的線性規劃問題(linear-programmingproblem)中,給定一個mXn的矩陣A,一個m維向量b和一個n維向量c,找出由n個元素組成的向量x,在由Ax≤b所給出的m個約束條件下,使目標函