1. 程式人生 > >資料結構——圖(7)——最短路徑與Dijkstra's Algorithm

資料結構——圖(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)。現在我們就來分析一下下面這張帶權圖的最短路徑及其權重值、
在這裡插入圖片描述

  1. 從開始的節點(A)開始,在向下一級鄰居移動之前,探索它的鄰居節點(基於優先順序)。
Vector<Vertex  *> startPath
	startPath.add(A,0)  //從節點A開始,權重為0
	pq.enqueue(startPath) //將初始的路徑存入優先順序佇列中

此時優先順序佇列的內容為:
在這裡插入圖片描述
已標記的點的集合為空:

在這裡插入圖片描述

  1. 接下來執行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

此時優先順序佇列的狀態為:
在這裡插入圖片描述
已訪問的集合為:
在這裡插入圖片描述

  1. 此時佇列不為空,繼續執行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,排在隊頭。已訪問的節點如下
在這裡插入圖片描述

  1. 我們繼續分析,此時下一輪出佇列的路徑時ADE,路徑尾元素是E,於是將E的節點的所有鄰居路徑新增進來:
    在這裡插入圖片描述
    已訪問的節點結合為:
    在這裡插入圖片描述

  2. 重複執行上述步驟,直到第一次V=I的出現。

  3. 當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”字型!