《演算法圖解》第七章迪克斯特拉學習心得
1、狄克斯特拉演算法 (Dijkstra's algorithm)
對於廣度優先搜尋找出的路徑,只適用於圖中邊的權值都相同的情況,如果權值不相同,可能找出的未必是最短路徑。如圖:
對於不同權值的圖,我們使用狄克斯特拉演算法尋找最短路徑,該演算法包含以下四個步驟:
- 找出“最便宜”的節點, 即可在最短時間內到達的節點。
- 更新該節點的鄰居的開銷, 其含義將稍後介紹。
- 重複這個過程, 直到對圖中的每個節點都這樣做了。
- 計算最終路徑。
2、術語
狄克斯特拉演算法用於每條邊都有關聯數字的圖, 這些數字稱為權重(weight) 。帶權重的圖稱為加權圖 (weighted graph) , 不帶權重的圖稱為非加權圖 (unweighted graph
狄克斯特拉演算法只適用於有向無環圖 (directed acyclic graph, DAG) 。
3、演算法舉例
- 問題背景
Rama, 想拿一本樂譜換架鋼琴。Alex說: “這是我最喜歡的樂隊Destroyer的海報, 我願意拿它換你的樂譜。 如果你再加5美元, 還可拿樂譜換我這張稀有的Rick Astley黑膠唱片。 ”Amy說: “哇, 我聽說這張黑膠唱片裡有首非常好聽的歌曲, 我願意拿我的吉他或架子鼓換這張海報或黑膠唱片。 ”Beethoven
這個圖中的節點是大家願意拿出來交換的東西, 邊的權重是交換時需要額外加多少錢。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、小結
- 廣度優先搜尋用於在非加權圖中查詢最短路徑。
- 狄克斯特拉演算法用於在加權圖中查詢最短路徑。
- 僅當權重為正時狄克斯特拉演算法才管用。
- 如果圖中包含負權邊, 請使用貝爾曼-福德演算法。