1. 程式人生 > >《演算法圖解》第七章迪克斯特拉學習心得

《演算法圖解》第七章迪克斯特拉學習心得

1、狄克斯特拉演算法 (Dijkstra's algorithm

對於廣度優先搜尋找出的路徑,只適用於圖中邊的權值都相同的情況,如果權值不相同,可能找出的未必是最短路徑。如圖:


對於不同權值的圖,我們使用狄克斯特拉演算法尋找最短路徑,該演算法包含以下四個步驟:

  • 找出最便宜的節點, 即可在最短時間內到達的節點。
  • 更新該節點的鄰居的開銷, 其含義將稍後介紹。
  • 重複這個過程, 直到對圖中的每個節點都這樣做了。
  • 計算最終路徑。

2、術語

狄克斯特拉演算法用於每條邊都有關聯數字的圖, 這些數字稱為權重(weight帶權重的圖稱為加權圖 (weighted graph , 不帶權重的圖稱為非加權圖 (unweighted graph

要計算非加權圖中的最短路徑, 可使用廣度優先搜尋 。 要計算加權圖中的最短路徑, 可使用狄克斯特拉演算法 。 圖還可能有環 , 而環類似下圖。

狄克斯特拉演算法只適用於有向無環圖 (directed acyclic graphDAG

3、演算法舉例

  • 問題背景

Rama, 想拿一本樂譜換架鋼琴。Alex說: 這是我最喜歡的樂隊Destroyer的海報, 我願意拿它換你的樂譜。 如果你再加5美元, 還可拿樂譜換我這張稀有的Rick Astley黑膠唱片。 ”Amy說: 哇, 我聽說這張黑膠唱片裡有首非常好聽的歌曲, 我願意拿我的吉他或架子鼓換這張海報或黑膠唱片。 Beethoven

驚呼: 我一直想要吉他, 我願意拿我的鋼琴換Amy的吉他或架子鼓。  下圖為他們的交換意願:


這個圖中的節點是大家願意拿出來交換的東西, 邊的權重是交換時需要額外加多少錢。Rama需要確定採用哪種路徑將樂譜換成鋼琴時需要支付的額外費用最少。 為此, 可以使用狄克斯特拉演算法! 別忘了, 狄克斯特拉演算法包含四個步驟。 在這個示例中, 你將完成所有這些步驟, 因此你也將計算最終路徑。

首先建立一個表格, 在其中列出每個節點的開銷。 這裡的開銷指的是達到節點需要額外支付多少錢。

在執行狄克斯特拉演算法的過程中, 你將不斷更新這個表。 為計算最終路徑, 還需在這個表中新增表示父節點的列。

第一步 : 找出最便宜的節點。 在這裡, 換海報最便宜,不需要支付額

外的費用。 
第二步 : 計算前往該節點的各個鄰居的開銷。

表中增加了架子鼓和吉他的開銷,這些開銷是通過海報交換所需支付的額外費用,所以他們的父節點均為海報。

再次執行第一步 : 下一個最便宜的節點是黑膠唱片——需要額外支付5美元。
再次執行第二步 : 更新黑膠唱片的各個鄰居的開銷。

對照上表,我們發現吉他和架子鼓的開銷都更新了,這意味著經黑膠唱片前往架子鼓吉他的開銷更低, 因此你將這些樂器的父節點改為黑膠唱片。

下一個最便宜的是吉他, 因此更新其鄰居的開銷。

於是,你計算出了到達鋼琴的開銷——40,而它的父節點為吉他。

最後, 對最後一個節點——架子鼓, 做同樣的處理。


如果用架子鼓換鋼琴, Rama需要額外支付的費用更少。 因此, 採用最便宜的交換路徑時, Rama需要額外支付35美元 。

最短路徑的確定:找到鋼琴的父節點(架子鼓),通過沿父節點回溯, 便得到了完整的交換路徑。


注意:不能將狄克斯特拉演算法用於包含負權邊的圖 。 在包含負權邊的圖中, 要找出最短路徑, 可使用另一種演算法——貝爾曼-福德演算法 (Bellman-Ford algorithm) 。 

4、演算法實現


python程式碼實現:

graph={}
#儲存起點的鄰居和權值
graph["start"]={}
graph["start"]["a"]=6
graph["start"]["b"]=2
#新增其他節點和鄰居
graph["a"]={}
graph["a"]["fin"]=1

graph["b"]={}
graph["b"]["a"]=3
graph["b"]["fin"]=5

graph["fin"]={}#終點沒有任何鄰居
#儲存父節點
parents={}
parents["a"]="start"
parents["b"]="start"
parents["fin"]=None

#記錄處理過的節點
processed=[]
#儲存每個節點的開銷
infinity=float("inf")
costs={}
costs["a"]=6
costs["b"]=2
costs["fin"]=infinity
def find_lower_cost_node(costs):
    lowest_cost=float("inf")
    lowest_cost_node=None
    for node in costs:#遍歷所有的節點
        cost=costs[node]
        if cost<lowest_cost and node not in processed:#如果當前節點的開銷更低且未處理過
            lowest_cost=cost
            lowest_cost_node=node#就將其視為開銷最低的節點
    return lowest_cost_node
node=find_lower_cost_node(costs)#在未處理的節點中找出開銷最小的節點
while node is not None:#這個while迴圈在所有節點都被處理過後結束
    cost=costs[node]
    neighbors=graph[node]
    for n in neighbors.keys():#遍歷當前節點的所有鄰居
        new_cost=cost+neighbors[n]
        if costs[n]>new_cost:#如果經當前節點前往該鄰居更近
            costs[n]=new_cost#就更新該鄰居的開銷
            parents[n]=node#同時將該鄰居的父節點設定為當前節點
    processed.append(node)#將當前節點標記為處理過
    node=find_lower_cost_node(costs)#找出接下來要處理的節點, 並迴圈

結果輸出:


5、小結

  • 廣度優先搜尋用於在非加權圖中查詢最短路徑。
  • 狄克斯特拉演算法用於在加權圖中查詢最短路徑。
  • 僅當權重為正時狄克斯特拉演算法才管用。
  • 如果圖中包含負權邊, 請使用貝爾曼-福德演算法。