Dijkstra 算法

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 中取出最小的值,并返回。

思路:

  1. 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)

  2. (cost,v1,path) = heappop(q)

    • 這行代碼會(huì)得到 q 中的最小值,也就是在算法描述部分提到的 k,用算法描述為:cost=min(cost1,cost2...)
  3. seen.add(v1)

    • 這行代碼對(duì)應(yīng)算法描述的“ k 加到 V 集合中,將 k 從 U 集合刪除”

    • 這個(gè)時(shí)候的 k 已經(jīng)成為了中間點(diǎn) v

  4. 查找 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容