第七章 狄克斯特拉算法
1 使用狄克斯特拉算法(Dijkstra’s algorithm)
用下圖舉個🌰:

該算法的四個步驟:
①找出"最便宜的節點",即可在最短時間內到達的節點,先找出
②更新該節點的鄰居的開銷
③重復這個過程,直到對圖中的每個節點都這樣做了
④計算最終路徑
第一步:找出最便宜的節點,假設需要時間無窮大,節點B是最近的——2分鍾。

第二步:計算經節點B前往其各個鄰居所需要的時間。

找到一條前往節點A的更短路徑。
對於節點B的鄰居,如果找到前往它的更短路徑,就更新其開銷。在這里,你找到了:
前往節點A的更短路徑(時間從6分鍾縮短到5分鍾);前往終點的更短路徑(時間從無窮大縮短到7分鍾)。
第三步:重復!
重復第一步:找出可在最短時間內前往的節點。你對節點B執行了第二步,除節點B外,可在最短時間內前往的節點是節點A。

重復第二步:更新節點A的所有鄰居的開銷。

你發現前往終點的時間為6分鍾!
你對每個節點都運行了狄克斯特拉算法(無需對終點這樣做)。現在,你知道:前往節點B需要2分鍾;前往節點A需要5分鍾;前往終點需要6分鍾。

廣度優先搜索來查找兩點之間的最短路徑,那時“最短路徑”的意思是段數最少。在狄克斯特拉算法中,你給每段都分配了一個數字或權重,因此狄克斯特拉算法找出的是總權重最小的路徑。
比較圖:

2 術語
該算法用於每條邊都有關聯數字的圖,這些數字稱為權重(weight)
帶權重的圖為加權圖(weighted graph),不帶權重的圖為非加權圖(unweighted graph)
計算非加權圖中的最短路徑,可使用廣度優先搜索。計算加權圖中的最短路徑,可使用狄克斯特拉算法。
可能會有環的存在:
繞環的路徑不可能是最短的路徑,在無向圖中,每條邊都是一個環。狄克斯特拉算法只適合於有向無環圖(directed acyclic graph,DAG)
3 換鋼琴
舉個🌰:

首先,創建一個表(和之前一樣無窮大):

第一步:找出最便宜的節點。換海報
第二步:計算前往該節點的各個鄰居的開銷。

再次執行第一步:下一個最便宜的節點是黑膠唱片——加5美元。你更新了架子鼓和吉他的開銷!這意味着經“黑膠唱片”前往“架子鼓”和“吉他”的開銷更低,因此你將這些樂器的父節點改為黑膠唱片。
再次執行第二步:更新黑膠唱片的各個鄰居的開銷。

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

你終於計算出了用吉他換鋼琴的開銷,於是你將其父節點設置為吉他。最后,對最后一個節點——架子鼓,做同樣的處理。

我們知道了最短路徑開銷是35,如何確定路徑呢?
先找出鋼琴的父節點:架子鼓。架子鼓的父節點:黑膠唱片。黑膠唱片的父節點:樂譜。得到完整的路徑。

最短路徑指的並不一定是物理距離,也可能是讓某種度量指標最小。
4 負權邊
舉個🌰:

第二種方式的開銷少2美元,應采取這種方式。但是,如果有負權變,就不能使用狄克斯特拉算法。
因為該算法假設:
對於處理過的海報節點,沒有前往該節點的更短路徑。這種假設僅在沒有負權邊時才成立。因此,不能將狄克斯特拉算法用於包含負權邊的圖。在包含負權邊的圖中,要找出最短路徑,可使用另一種算法——貝爾曼-福德算法(Bellman-Fordalgorithm)。
5 實現
以下圖為🌰,用代碼實現狄克斯特拉算法,

首先,需要三個散列表

第一個散列表是整個圖的散列表,第二個散列表是開銷,第三個散列表是父子節點
還需要一個數組,用於記錄處理過的節點,因為對於同一個節點,你不用處理多次。processed = [ ]
算法如下:

代碼如下:(書中有詳細的每一步過程描述)
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"] = {} infinity = float("inf") #散列表是開銷 costs = {} costs["a"] = 6 costs["b"] = 2 costs["fin"] = infinity parents = {} #散列表是父子節點 parents["a"] = "start" parents["b"] = "start" parents["fin"] = None processed = [] def find_lowest_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_lowest_cost_node(costs) #在未處理的節點中找出開銷最小的節點 while node is not None: #在所有節點都被處理后結束 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_lowest_cost_node(costs) #找出接下來要處理的節點,並循環 print(costs) print(parents)

練習
7.1 在下面的各個圖中,從起點到終點的最短路徑的總權重分別是多少?

A:8 B:60 C:負權邊,無法用狄克斯特拉算法無法找出最短路徑。
6 小結
廣度優先搜索用於在非加權圖中查找最短路徑。
狄克斯特拉算法用於在加權圖中查找最短路徑。
僅當權重為正時狄克斯特拉算法才管用。
如果圖中包含負權邊,請使用貝爾曼-福德算法。
