1. 程式人生 > >經典演算法研究系列 二之續 徹底理解Dijkstra演算法

經典演算法研究系列 二之續 徹底理解Dijkstra演算法

               

       經典演算法研究系列:二之續、徹底理解Dijkstra演算法 

作者:July   二零一一年二月十三日。參考程式碼:introduction to algorithms,Second Edition。---------------------------------------

本文由單源最短路徑路徑問題開始,而後描述Bellman-Ford演算法,到具體闡述Dijkstra演算法,闡述詳細剖析Dijkstra演算法的每一個步驟,教你徹底理解此Dijkstra演算法。 

一、單源最短路徑問題我們知道,單源最短路徑問題:已知圖G=(V,E),要求找出從某個定源頂點s<-V,到每個v<-V的最短路徑。簡單來說,就是一個圖G中,找到一個定點s,然後以s為起點,要求找出s到圖G中其餘各個點的最短距離或路徑。

此單源最短路徑問題有以下幾個變形:I、  單終點最短路徑問題:  每個頂點v到指定終點t的最短路徑問題。即單源最短路徑問題的相對問題。II、 單對頂點最短路徑問題:給定頂點u和v,找出從u到v的一條最短路徑。

III、每對頂點間最短路徑問題:針對任意每倆個頂點u和v,找出從u到v的最短路徑。最簡單的想法是,將每個頂點作為源點,執行一次單源演算法即可以解決這個問題。當然,還有更好的辦法,日後在本BOlG內闡述。 

二、Bellman-Ford 演算法1、迴路問題一條最短路徑不能包含負權迴路,也不能包含正權迴路。一些最短路徑的演算法,如Dijkstra 演算法,要求圖中所有的邊的權值都是非負的,如在公路地圖上,找一條從定點s到目的頂點v的最短路徑問題。

2、Bellman-Ford 演算法而Bellman-Ford 演算法,則允許輸入圖中存在負權邊,只要不存在從源點可達的負權迴路,即可。簡單的說,圖中可以存在負權邊,但此條負權邊,構不成負權迴路,不影響迴路的形成。且,Bellman-Ford 演算法本身,便是可判斷圖中是否存在從源點可達的負權迴路,若存在負權迴路,演算法返回FALSE,若不存在,返回TRUE。

Bellman-Ford 演算法的具體描述BELLMAN-FORD(G, w, s)1  INITIALIZE-SINGLE-SOURCE(G, s)   //對每個頂點初始化 ,O(V) 2  for i ← 1 to |V[G]| - 13       do for each edge (u, v) ∈ E[G]4              do RELAX(u, v, w)    //針對每個頂點(V-1個),都運用鬆弛技術O(E),計為O((v-1)*E))

5  for each edge (u, v) ∈ E[G]6       do if d[v] > d[u] + w(u, v)7             then return FALSE     //檢測圖中每條邊,判斷是否包含負權迴路,                                    //若d[v]>d[u]+w(u,v),則表示包含,返回FALSE,8  return TRUE                      //不包含負權迴路,返回TRUE

Bellman-Ford 演算法的時間複雜度,由上可得為O(V*E)。

3、關於判斷圖中是否包含負權迴路的問題:根據定理,我們假定,u是v的父輩,或父母,那麼當G(V,E)是一個有向圖或無向圖(且不包含任何負權迴路),s<-V,s為G的任意一個頂點,則對任意邊(u,v)<-V,有          d[s,v] <= d[s,u]+1此定理的詳細證明,可參考演算法導論一書上,第22章中引理22.1的證明。或者根據第24章中通過三角不等式論證Bellman-Ford演算法的正確性,也可得出上述定理的變形。

即假設圖G中不包含負權迴路,可證得   d[v]=$(s,v)      <=$(s,u)+w(u,v)  //根據三角不等式      =d[u]+w[u,v]所以,在不包含負權迴路的圖中,是可以得出d[v]<=d[u]+w(u,v)。

於是,就不難理解,在上述Bellman-Ford 演算法中, if d[v] > d[u]+w(u,v),=> 包含負權迴路,返回FASLE else if =>不包含負權迴路,返回TRUE。

ok,咱們,接下來,立馬切入Dijkstra 演算法。

三、深入淺出,徹底解剖Dijkstra 演算法I、鬆弛技術RELAX的介紹Dijkstra 演算法使用了鬆弛技術,對每個頂點v<-V,都設定一個屬性d[v],用來描述從源點s到v的最短路徑上權值的上界,稱為最短路徑的估計。

首先,得用O(V)的時間,來對最短路徑的估計,和對前驅進行初始化工作。INITIALIZE-SINGLE-SOURCE(G, s)1  for each vertex v ∈ V[G]2       do d[v] ← ∞3          π[v] ← NIL      //O(V)4  d[s] 0

RELAX(u, v, w)1  if d[v] > d[u] + w(u, v)2     then d[v] ← d[u] + w(u, v)3          π[v] ← u        //O(E)圖。

II、Dijkstra 演算法此Dijkstra 演算法分三個步驟,INSERT (第3行), EXTRACT-MIN (第5行), 和DECREASE-KEY(第8行的RELAX,呼叫此減小關鍵字的操作)。

DIJKSTRA(G, w, s)1  INITIALIZE-SINGLE-SOURCE(G, s)    //對每個頂點初始化 ,O(V) 2  S ← Ø3  Q ← V[G]            //INSERT,O(1)4  while Q ≠ Ø5      do u ← EXTRACT-MIN(Q)        //簡單的O(V*V);二叉/項堆,和FIB-HEAP的話,則都為O(V*lgV)。6         S ← S ∪{u}7         for each vertex v ∈ Adj[u]8             do RELAX(u, v, w)      //簡單方式:O(E),二叉/項堆,E*O(lgV),FIB-HEAP,E*O(1)

四、Dijkstra 演算法的執行時間在繼續闡述之前,得先宣告一個問題,DIJKSTRA(G,w,s)演算法中的第5行,EXTRACT-MIN(Q),最小優先佇列的具體實現。而Dijkstra 演算法的執行時間,則與此最小優先佇列的採取何種具體實現,有關。

最小優先佇列三種實現方法:1、利用從1至|V| 編好號的頂點,簡單地將每一個d[v]存入一個數組中對應的第v項,如上述DIJKSTRA(G,w,s)所示,Dijkstra 演算法的執行時間為O(V^2+E)。

2、如果是二叉/項堆實現最小優先佇列的話,EXTRACT-MIN(Q)的執行時間為O(V*lgV),所以,Dijkstra 演算法的執行時間為O(V*lgV+E*lgV),若所有頂點都是從源點可達的話,O((V+E)*lgV)=O(E*lgV)。當是稀疏圖時,則E=O(V^2/lgV),此Dijkstra 演算法的執行時間為O(V^2)。

3、採用斐波那契堆實現最小優先佇列的話,EXTRACT-MIN(Q)的執行時間為O(V*lgV),所以,此Dijkstra 演算法的執行時間即為O(V*lgV+E)。

綜上所述,此最小優先佇列的三種實現方法比較如下:      EXTRACT-MIN + RELAXI、  簡單方式:  O(V*V + E*1)II、 二叉/項堆: O(V*lgV + |E|*lgV)       源點可達:O(E*lgV)       稀疏圖時,有E=o(V^2/lgV),            =>   O(V^2)  III、斐波那契堆:O(V*lgV + E)

當|V|<<|E|時,採用DIJKSTRA(G,w,s)+ FIB-HEAP-EXTRACT-MIN(Q),即斐波那契堆實現最小優先佇列的話,優勢就體現出來了。

五、Dijkstra 演算法 + FIB-HEAP-EXTRACT-MIN(H),斐波那契堆實現最小優先佇列由以上內容,我們已經知道,用斐波那契堆來實現最小優先佇列,可以將執行時間提升到O(VlgV+E)。|V|個EXTRACT-MIN 操作,每個平攤代價為O(lgV),|E|個DECREASE-KEY操作的每個平攤時間為O(1)。

下面,重點闡述DIJKSTRA(G, w, s)中,斐波那契堆實現最小優先佇列的操作。

由上,我們已經知道,DIJKSTRA演算法包含以下的三個步驟:INSERT (第3行), EXTRACT-MIN (第5行), 和DECREASE-KEY(第8行的RELAX)。

先直接給出Dijkstra 演算法 + FIB-HEAP-EXTRACT-MIN(H)的演算法:DIJKSTRA(G, w, s)1  INITIALIZE-SINGLE-SOURCE(G, s)2  S ← Ø3  Q ← V[G]   //第3行,INSERT操作,O(1)4  while Q ≠ Ø5      do u ← EXTRACT-MIN(Q)   //第5行,EXTRACT-MIN操作,V*lgV6         S ← S ∪{u}7         for each vertex v ∈ Adj[u]8             do RELAX(u, v, w)  //第8行,RELAX操作,E*O(1)

FIB-HEAP-EXTRACT-MIN(H)  //平攤代價為O(lgV) 1  z ← min[H] 2  if z ≠ NIL 3     then for each child x of z 4              do add x to the root list of H 5                 p[x] ← NIL 6          remove z from the root list of H 7          if z = right[z] 8             then min[H] ← NIL 9             else min[H] ← right[z]10                  CONSOLIDATE(H)   11          n[H] ← n[H] - 112  return z

--------------------------------------------------------------------------------------

六、Dijkstra 演算法 +fibonacci堆各項步驟的具體分析    ok,接下來,具體分步驟闡述以上各個操作:

第3行的INSERT操作:FIB-HEAP-INSERT(H, x)   //平攤代價,O(1). 1  degree[x] ← 0 2  p[x] ← NIL 3  child[x] ← NIL 4  left[x] ← x 5  right[x] ← x 6  mark[x] ← FALSE 7  concatenate the root list containing x with root list H 8  if min[H] = NIL or key[x] < key[min[H]] 9     then min[H] ← x10  n[H] ← n[H] + 1

第5行的EXTRACT-MIN操作: FIB-HEAP-EXTRACT-MIN(H)  //平攤代價為O(lgV) 1  z ← min[H] 2  if z ≠ NIL 3     then for each child x of z 4              do add x to the root list of H 5                 p[x] ← NIL 6          remove z from the root list of H 7          if z = right[z] 8             then min[H] ← NIL 9             else min[H] ← right[z]10                  CONSOLIDATE(H)   //CONSOLIDATE演算法在下面,給出。11          n[H] ← n[H] - 112  return z

下圖是FIB-HEAP-EXTRACT-MIN 的過程示意圖

CONSOLIDATE(H) 1 for i ← 0 to D(n[H]) 2      do A[i] ← NIL 3 for each node w in the root list of H 4      do x ← w 5         d ← degree[x]        //子女數 6         while A[d] ≠ NIL 7             do y ← A[d]       8                if key[x] > key[y] 9                   then exchange x <-> y10                FIB-HEAP-LINK(H, y, x)  //下面給出。11                A[d] ← NIL12                d ← d + 113         A[d] ← x14 min[H] ← NIL15 for i ← 0 to D(n[H])16      do if A[i] ≠ NIL17            then add A[i] to the root list of H18                 if min[H] = NIL or key[A[i]] < key[min[H]]19                    then min[H] ← A[i]

FIB-HEAP-LINK(H, y, x)   //y連結至 x。1  remove y from the root list of H2  make y a child of x, incrementing degree[x]3  mark[y] ← FALSE

第8行的RELAX的操作,已上已經給出:RELAX(u, v, w)1  if d[v] > d[u] + w(u, v)2     then d[v] ← d[u] + w(u, v)3          π[v] ← u        //O(E)

一般來說,在Dijkstra 演算法中,DECREASE-KEY的呼叫次數遠多於EXTRACT-MIN的呼叫,所以在不增加EXTRACT-MIN 操作的平攤時間前提下,儘量減小DECREASE-KEY操作的平攤時間,都能獲得對比二叉堆更快的實現。

以下,是二叉堆,二項堆,斐波那契堆的各項操作的時間複雜度的比較:

操作                  二叉堆(最壞)       二項堆(最壞)     斐波那契堆(平攤)

__________________________________MAKE-HEAP        Θ(1)                  Θ(1)                Θ(1)INSERT               Θ(lg n)              O(lg n)            Θ(1)MINIMUM           Θ(1)                  O(lg n)             Θ(1)EXTRACT-MIN     Θ(lg n)              Θ(lg n)            O(lg n)UNION               Θ(n)                  O(lg n)            Θ(1)DECREASE-KEY   Θ(lg n)             Θ(lg n)              Θ(1)DELETE              Θ(lg n)              Θ(lg n)             O(lg n)

斐波那契堆,日後會在本BLOG內,更進一步的深入與具體闡述。且同時,此文,會不斷的加深與擴充套件。完。

本人July對本部落格所有任何文章、內容和資料享有版權。轉載務必註明作者本人及出處,並通知本人。謝謝。

July、二零一一年二月十三日。