資料結構——圖(7)——最短路徑與Dijkstra's Algorithm
帶權圖
在圖中,給每一條路徑帶上一定的權重,這樣的圖我們稱為帶權圖。如下圖所示: 我們現在來回顧一下BFS跟DFS的基本思想:
-
深度優先搜尋:繼續沿著路徑搜尋,直到我們需要回溯,但這種方式不保證最短路徑。
-
廣度優先搜尋:檢視包含距離1的鄰居,然後是距離2的鄰居等的路徑,直到找到路徑,這種方式保證最短路徑。
但這兩種方法都是無法處理帶權圖的問題。舉個例子 由A到D的最短路徑是什麼?我們當然會毫不猶豫得回答,A->D。是的,確實是A到D。但是如果是這樣的帶權圖(即數字代表兩個節點之間的距離)呢? A->D還是最短路徑嗎?顯然不是。應該是abc,因為三者的權重值加起來都不如ad的權重。所以用B(D)FS是無法找到最短路徑的。 儘管我們知道BFS演算法保證了兩個節點中的最短距離,但是在帶權圖中,兩個節點間的最小花費值才是最短路徑。
Dijstra’s Algorithm
回想一下我們之前是怎麼實現BFS演算法的。如果我們使用BFS來查詢路徑(忽略權重),我們將使用佇列來儲存每個路徑。類似於這種做法,我們可以利用一種優先佇列來實現這種演算法。這種演算法稱為“Dijkstra演算法”,他使用優先順序佇列來對每條路徑進行排隊。 我們先來對比一下兩種演算法的虛擬碼:
BFS虛擬碼
bfs from v1 to v2: //建立一個佇列q來儲存走過的路徑path(可以用vector來儲存路徑) create a queue of paths (a vector), q //將節點v1的路徑入隊 q.enqueue(v1 path) //當q不為空,並且節點V2未被訪問時 while q is not empty and v2 is not yet visited: //將q中的元素移出佇列,並且存入path中 path = q.dequeue() //將路徑中的最後元素賦值給節點型變數v v = last element in path //如果v未被訪問 if v is not visited: //標記節點V mark v as visited //如果節點是目標節點,停止執行 if v is the end vertex, we can stop. //遍歷V中所有未被標記的鄰居 for each unvisited neighbor of v: //將v節點的鄰居作為最後的元素構成新的路徑 make new path with v's neighbor as last element //將新的路徑排入佇列中 enqueue new path onto q
Dijstra’s Algorithm演算法的虛擬碼
bfs from v1 to v2: create a priority queue of paths (a vector), q //注意這裡是 priority queue q.enqueue(v1 path) while q is not empty and v2 is not yet visited: path = q.dequeue() v = last element in path mark v as visited if v is the end vertex, we can stop. for each unvisited neighbor of v: make new path with v's neighbor as last element enqueue new path onto q
因為這種演算法每次都是選擇最小的花費路徑,因此該演算法我們又稱為“貪婪演算法”(“greedy” algorithm)。現在我們就來分析一下下面這張帶權圖的最短路徑及其權重值、
- 從開始的節點(A)開始,在向下一級鄰居移動之前,探索它的鄰居節點(基於優先順序)。
Vector<Vertex *> startPath
startPath.add(A,0) //從節點A開始,權重為0
pq.enqueue(startPath) //將初始的路徑存入優先順序佇列中
此時優先順序佇列的內容為: 已標記的點的集合為空:
- 接下來執行while迴圈裡面的語句:
in while loop:
//將當前佇列的內容取出
curPath = pq.dequeue() (path is A, priority is 0)
//將路徑中的最後一個元素賦值給節點變數V
v = last element in curPath (v is A)
//標記變數V
mark v as visited
//將所有未訪問的鄰居路徑排入q,並根據新的邊長度更新優先順序
enqueue all unvisited neighbor paths onto q,
with updated priorities based on new edge length
此時優先順序佇列的狀態為: 已訪問的集合為:
- 此時佇列不為空,繼續執行while語句內的內容,具體內容變化如下:
in while loop:
//出佇列,現在AD排在隊頭,因此出去的為路徑AD,優先順序為3
curPath = pq.dequeue() (path is AD, priority is 3)
//該路徑中最後的一個元素為D,並賦值給節點元素
v = last element in curPath (v is D)
//標記節點V
mark v as visited
////將所有未訪問的鄰居路徑排入q,並根據新的邊長度更新優先順序
enqueue all unvisited neighbor paths onto q,
with updated priorities based on new edge length
此時優先順序佇列的內容為 即此時D的鄰居節點為G和E,由於W(A-> D -> E)= 3+1 = 4 < W(AB) = 6. 因此ADE優先於AB,排在隊頭。已訪問的節點如下
-
我們繼續分析,此時下一輪出佇列的路徑時ADE,路徑尾元素是E,於是將E的節點的所有鄰居路徑新增進來: 已訪問的節點結合為:
-
重複執行上述步驟,直到第一次V=I的出現。
-
當V = I 的時候,表明存在一條路徑使得A -> I.有佇列的優先順序可知,第一次出現的一定是權重總和最小的。(即開銷最小的)。對於上述的例子最後的狀態應該是這樣的:
標記集合為 (此時已經沒有可以入隊的路徑了,因為所有鄰居節點都被訪問過)。所以,最短的路徑就是ADEHI,權重為13.
關於Edsger Dijkstra
計算曆史Tidbit:Edsger Dijkstra 荷蘭學者Edsger Dijkstra是電腦科學領域的另一個巨頭。他是第一批稱自己為“程式設計師”的科學家之一(他因此而幾乎無法結婚!)他最初獲得理論物理學學位,但在1950年初被電腦迷住了 Dijkstra在許多計算領域都具有極大的影響力:編譯器,作業系統,併發程式設計,軟體工程,程式語言,演算法設計和教學(以及其他!) 很難確定哪些領域是他最著名的或者說最厲害的,因為他對CS的影響實在是太大了。 Dijkstra在使程式設計更具結構性方面也很有影響力 - 他撰寫了一篇名為“Goto Considered Harmful”的開創性論文(GOTO有害論),他抨擊了“goto”宣告的概念(存在於C ++中 ,現在幾乎不怎麼用到,如果有機會的話,就使用一下試試它!) 其中更酷的是,字母“ijk”在他的名字中是相鄰的(這就是為什麼我們在迴圈中使用i,j,k?)。他的早期論文都是是手寫的,他的筆跡很漂亮。這個字型是“Edsger Dijkstra”字型!