Dijkstra 算法
前言
為了達(dá)到任意兩結(jié)點(diǎn)的最短路徑扳缕,我們有幾種算法可以實(shí)現(xiàn):Dijkstra 算法逸雹、Floyd 算法等等流酬。
Floyd 算法雖然可以得到一幅圖中任意兩點(diǎn)的最小 cost回官,但是我們?cè)诒绢}重點(diǎn)關(guān)注最短路徑 Shortest_path,若要采用 Floyd 算法來得到最短路徑 Shortest_path 不太方便城榛,所以我們決定采用 Dijkstra 算法。
該算法使用了廣度優(yōu)先搜索解決帶權(quán)有向圖的單源最短路徑問題态兴,算法最終得到一個(gè)最短路徑樹狠持。該算法常用于路由算法或者作為其他圖算法的一個(gè)子模塊。Dijkstra 算法無法解決負(fù)權(quá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ì)于無向無權(quán)圖鸭限,肯定對(duì)稱蜕径。
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)]
沒什么差別,都是我們用來存儲(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观堂,然后開始迭代让网。
在每一次迭代過程中,取得 U 中距離 k 最短的點(diǎn) k师痕,將 k 加到 V 集合中溃睹,將 k 從 U 集合刪除,再將 k 設(shè)為中間點(diǎn) v胰坟。重復(fù)此過程直到 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 集合涣易,記錄了所有已訪問的點(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)來存儲(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 來存儲(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