Dijkstra 算法
前言
為了達(dá)到任意兩結(jié)點(diǎn)的最短路徑,我們有幾種算法可以實(shí)現(xiàn):Dijkstra 算法、Floyd 算法等等。
Floyd 算法雖然可以得到一幅圖中任意兩點(diǎn)的最小 cost,但是我們?cè)诒绢}重點(diǎn)關(guān)注最短路徑 Shortest_path,若要采用 Floyd 算法來(lái)得到最短路徑 Shortest_path 不太方便,所以我們決定采用 Dijkstra 算法。
該算法使用了廣度優(yōu)先搜索解決帶權(quán)有向圖的單源最短路徑問(wèn)題,算法最終得到一個(gè)最短路徑樹(shù)。該算法常用于路由算法或者作為其他圖算法的一個(gè)子模塊。Dijkstra 算法無(wú)法解決負(fù)權(quán)重的問(wèn)題,但所幸在本題中不考慮負(fù)權(quán)重。
準(zhǔn)備工作
如何描述一幅圖?
-
用Python的二維列表(2-D list)描述的圖,這是鄰接矩陣的經(jīng)典表示方法,每行代表一個(gè)結(jié)點(diǎn),每列代表一個(gè)結(jié)點(diǎn),如果對(duì)應(yīng)的值為1,則表示兩個(gè)結(jié)點(diǎn)鄰接,如果為 M 則不鄰接,對(duì)于無(wú)向無(wú)權(quán)圖,肯定對(duì)稱(chēng)。
nodes = ['s1', 's2', 's3', 's4'] M = float("inf") # M means a large number graph_list = [ [M, 1, 1, 1], [1, M, M, 1], [1, M, M, 1], [1, 1, 1, M], ]
-
用 Python 的列表與元組(tuple)描述的圖,這實(shí)際上存儲(chǔ)的是每條邊的信息,每個(gè)括號(hào)內(nèi)的內(nèi)容依次為:(tail,head,cost)。
graph_edges = [ (‘s1’, ‘s2’, 1), (‘s1’, ‘s3’, 1), (‘s1’, ‘s4’, 1), (‘s2’, ‘s1’, 1), (‘s2’, ‘s4’, 1), (‘s3’, ‘s1’, 1), (‘s3’, ‘s4’, 1), (‘s4’, ‘s1’, 1), (‘s4’, ‘s2’, 1), (‘s4’, ‘s3’, 1), ]
-
用 Python 的字典(dict)描述的圖,這種表示方法很靈活,每個(gè)key代表一個(gè)結(jié)點(diǎn),該結(jié)點(diǎn)作為tail,其鄰接的head及它們之間的cost存儲(chǔ)在value里,可想而知,對(duì)于每個(gè) tail,可能有多個(gè) head,所以 value 其實(shí)是一個(gè)列表,每個(gè)列表項(xiàng)是個(gè)兩元組 (head, cost)。
graph_dict = { ‘s1’:[(‘s2’,1),(‘s3’,1),(‘s4’,1)], ‘s2’:[(‘s1’,1), (‘s4’,1)], ‘s3’:[(‘s1’,1), (‘s4’,1)], ‘s4’:[(‘s1’,1),(‘s2’,1),(‘s3’,1)], }
在有些Python代碼中,也有這樣的形式:
‘s1’:{‘s2’:1,‘s3’:1,‘s4’:1}
,這和我們的‘s1’:[(‘s2’,1),(‘s3’,1),(‘s4’,1)]
沒(méi)什么差別,都是我們用來(lái)存儲(chǔ)數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)。-
這三種表示方法在是可以互相轉(zhuǎn)化,在此我們給出 graph_list -> graph_edges 以及 graph_edges->graph_dict 的轉(zhuǎn)化算法。
# graph_list -> graph_edges graph_edges = [] for i in nodes: for j in nodes: if i!=j and graph_list[nodes.index(i)][nodes.index(j)]!=M: graph_edges.append((i,j,graph_list[nodes.index(i)][nodes.index(j)])) # graph_edges->graph_dict graph_dict = defaultdict(list) for tail,head,cost in graph_edges: graph_dict[tail].append((head,cost))
算法描述
將圖所有點(diǎn)的集合 S 分為兩部分,V 和 U。
V 集合是已經(jīng)得到最短路徑的點(diǎn)的集合,在初始情況下 V 中只有源點(diǎn) s,U 是還未得到最短路徑點(diǎn)的集合,初始情況下是除 s 的所有點(diǎn)。因?yàn)槊看蔚枰该鳟?dāng)前正在迭代的 V 集合中的某點(diǎn),所以將該點(diǎn)設(shè)為中間點(diǎn)。自然,首先應(yīng)將 s 設(shè)為中間點(diǎn) k,然后開(kāi)始迭代。
在每一次迭代過(guò)程中,取得 U 中距離 k 最短的點(diǎn) k,將 k 加到 V 集合中,將 k 從 U 集合刪除,再將 k 設(shè)為中間點(diǎn) v。重復(fù)此過(guò)程直到 U 集合為空。
Python 實(shí)現(xiàn) Dijkstra 算法
一般而言,若想尋找給定兩點(diǎn)的最短路徑,Dijkstra 算法必須傳入三個(gè)參數(shù),一個(gè)是圖的描述 graph_dict,一個(gè)是源點(diǎn) from_node,一個(gè)是終點(diǎn) to_node。
-
核心代碼如下:
def dijkstra(graph_dict, from_node, to_node): cost = -1 ret_path=[] q, seen = [(0,from_node,())], set() while q: (cost,v1,path) = heappop(q) if v1 not in seen: seen.add(v1) path = (v1, path) if v1 == to_node: # Find the to_node!!! break; for v2,c in graph_dict.get(v1, ()): if v2 not in seen: heappush(q, (cost+c, v2, path)) # Check the way to quit 'while' loop!!! if v1 != to_node: print("There is no node: " + str(to_node)) cost = -1 ret_path=[] # IF there is a path from from_node to to_node, THEN format the path!!! if len(path)>0: left = path[0] ret_path.append(left) right = path[1] while len(right)>0: left = right[0] ret_path.append(left) right = right[1] ret_path.reverse() return cost,ret_path
測(cè)試
-
不失一般性,給定一個(gè)帶權(quán)有向圖:
graph_list = [ [0,30,15,M,M,M], [5,0,M,M,20,30], [M,10,0,M,M,15], [M,M,M,0,M,M], [M,M,M,10,0,M], [M,M,M,30,10,0] ]
-
其表示的圖如下:
graph -
測(cè)試一下 s1 到 s6 的最短路徑:
test 可以看到,dijkstra 函數(shù)得到了正確的最短路徑以及 cost 值。
算法詳解
-
這里用到了堆排序的 heapq 模塊,注意它的 heappop(q) 與heappush(q,item) 方法:
heapq.heappush(heap, item): 將 item 壓入到堆數(shù)組 heap 中。如果不進(jìn)行此步操作,后面的 heappop() 失效
heapq.heappop(heap): 從堆數(shù)組 heap 中取出最小的值,并返回。
思路:
-
q, seen = [(0,from_node,())], set()
q 記錄了中間點(diǎn) v 與 U 集合中哪些點(diǎn)鄰接,這些鄰接點(diǎn)為 k1、k2...,并且在 q 中的存儲(chǔ)形式為:[(cost1,k1,path1),(cost2,k2,path2)...]
seen 就是算法描述部分提到的 V 集合,記錄了所有已訪問(wèn)的點(diǎn)
-
(cost,v1,path) = heappop(q)
- 這行代碼會(huì)得到 q 中的最小值,也就是在算法描述部分提到的 k,用算法描述為:cost=min(cost1,cost2...)
-
seen.add(v1)
這行代碼對(duì)應(yīng)算法描述的“ k 加到 V 集合中,將 k 從 U 集合刪除”
這個(gè)時(shí)候的 k 已經(jīng)成為了中間點(diǎn) v
-
查找 U 集合中所有與中間點(diǎn) v 鄰接的點(diǎn) k1、k2... :
for v2,c in graph_dict.get(v1, ()): if v2 not in seen: heappush(q, (cost+c, v2, path))
- 把 k1、k2... push 進(jìn)入 q 中,回到第 2 點(diǎn)
利用 dijkstra 得到圖中所有最短路徑
-
我們準(zhǔn)備在此基礎(chǔ)上加以改進(jìn),利用 Dijkstra 算法得到任意兩個(gè)結(jié)點(diǎn)之間的最短路徑,為了達(dá)到這個(gè)目的,我們?cè)谒惴ǖ淖詈笠幸粋€(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)這些最短路徑,如果使用 Python,這個(gè)數(shù)據(jù)結(jié)構(gòu)應(yīng)該是像下面這樣的:
Shortest_path_dict = { 's1': {'s2': ['s1', 's3', 's2'], 's3': ['s1', 's3'] }, 's2': {'s1': ['s2', 's1'], 's3': ['s2', 's1', 's3'}, 's3': {'s1': ['s3', 's2', 's1'], 's2': ['s3', 's2'] }, }
-
它存儲(chǔ)了下面這幅圖的所有最短路徑:
graph2 我們要想得到任意兩點(diǎn)的最短路徑,要用 Shortest_path_dict 來(lái)存儲(chǔ)所有可能的最短路徑,于是再創(chuàng)建一個(gè)新的函數(shù) dijkstra_all,該函數(shù)僅僅只需要接受一個(gè)參數(shù):圖的描述 graph_dict,然后返回 Shortest_path_dict,在 dijkstra_all 中需要循環(huán)調(diào)用 dijkstra 函數(shù)。
-
核心代碼如下:
def dijkstra_all(graph_dict): Shortest_path_dict = defaultdict(dict) for i in nodes: for j in nodes: if i != j: cost,Shortest_path = dijkstra(graph_dict,i,j) Shortest_path_dict[i][j] = Shortest_path return Shortest_path_dict
-
不失一般性,我們采用帶權(quán)有向圖測(cè)試我們的算法,圖的描述與前文測(cè)試 dijkstra 算法時(shí)一致,在此直接調(diào)用 dijkstra_all函數(shù),傳入 graph_dict,得到的結(jié)果截圖如下:
test2 看最后的輸出,得到了我們想要的 Shortest_path_dict
源碼:https://github.com/edisonleolhl/DataStructure-Algorithm/blob/master/Graph/ShortestPath/dijkstra.py