Python小白的數學建模課-16.最短路徑演算法

youcans發表於2021-08-06

  • 最短路徑問題是圖論研究中的經典演算法問題,用於計算圖中一個頂點到另一個頂點的最短路徑。
  • 在圖論中,最短路徑長度與最短路徑距離卻是不同的概念和問題,經常會被混淆。
  • 求最短路徑長度的常用演算法是 Dijkstra 演算法、Bellman-Ford 演算法和Floyd 演算法,另外還有啟發式演算法 A*。
  • 『Python小白的數學建模課 @ Youcans』帶你從數模小白成為國賽達人。


1. 最短路徑問題

最短路徑問題是圖論研究中的經典演算法問題,用於計算圖中一個頂點到另一個頂點的最短路徑。

最短路徑問題有幾種形式:確定起點的最短路徑,確定終點的最短路徑,確定起點和終點的最短路徑,全域性最短路徑問題。

1.1 最短路徑長度與最短路徑距離

在日常生活中,最短路徑長度與最短路徑距離好像並沒什麼區別。但在圖論中最短路徑長度與最短路徑距離卻是不同的概念和問題,經常會被混淆。

圖論中有無權圖和有權圖,無權圖中的邊沒有權,賦權圖的邊帶有權,可以表示距離、時間、費用或其它指標。在問題文字描述中,往往並不直接指出是無權圖還是有權圖,這時就要特別注意最短路徑與最短加權路徑的區別。

路徑長度是把每個頂點到相鄰頂點的長度記為 1,而不是指這兩個頂點之間道路的距離——兩個頂點之間的道路距離是 連線邊的權(weight)。

通俗地說,路徑長度可以認為是飛行棋的步數,或者公交站點的站數,相鄰頂點之間為一步,相隔幾個頂點就是幾站。路徑長度是從路徑起點到終點的步數,計算最短路徑是要計算從起點到終點步數最少的路徑。

如果問題不涉及相鄰頂點間的距離,要計算從起點到終點的最短路徑及對應的最短路徑長度,是指這條路徑從起點到終點有幾步(站),在圖論中稱為最短路徑長度。但是,如果問題給出相鄰頂點之間的道路長度或距離,姑且稱為各路段的距離,要計算從起點到終點的最短路徑及對應的最短距離,顯然並不是要找經過最少步數的路徑,而是在找路徑中各路段的距離之和最小的路徑,在圖論中稱為最短加權路徑長度——這裡權重是路段距離。

相鄰頂點的連線邊的權,不僅可以是路段距離,也可以是時間、費用等指標。問題就變成尋求最短時間、最低成本的路徑,這實際上也是最短加權路徑長度問題。


1.2 最短路徑的常用演算法

求解最短路徑長度的常用演算法是 Dijkstra 演算法、Bellman-Ford 演算法和Floyd 演算法,另外還有啟發式演算法 A*。

1.2.1 Dijkstra 演算法

Dijkstra 演算法是經典的最短路徑演算法,在資料結構、圖論、運籌學中都是教學的基本演算法。有趣的是,在資料結構中 Dijkstra 演算法通常是按貪心法講述,而在運籌學中則被認為是動態規劃法。

Dijkstra演算法從起點開始,採用貪心法策略,每次遍歷距離起點最近且未訪問過的鄰接頂點, 層層擴充套件直到終點為止。

Dijkstra演算法可以求出加權最短路徑的最優解,演算法的時間複雜度為 O(n^2)。如果邊數遠小於 n^2,可以用堆結構將複雜度降為O((m+n)log(n))。

Dijkstar演算法不能處理負權邊,這是由於貪心法的選擇規則決定的。

1.2.2 Bellman-Ford 演算法

Bellman-Ford 演算法是求含負權圖的單源最短路徑演算法。演算法原理是對圖進行 V-1次鬆弛操作,得到所有可能的最短路徑。

Bellman-Ford 演算法可以處理負權邊。其基本操作“擴充”是在深度上搜尋,而“鬆弛”操作則在廣度上搜尋,因此可以對負權邊進行操作而不影響結果。

Bellman-Ford 演算法的效率很低,時間複雜度高達 O(V*E),V、E 分別是頂點和邊的數量。SPFA 是 Bellman-Ford 的佇列優化,通過維護一個佇列極大地減少了重複計算,時間複雜度為 O(k*E) 。

Dijkstra 演算法在求解過程中,起點到各頂點的最短路徑求出後就不變了。Bellman演算法在求解過程中,每次迴圈都要修改所有頂點間的距離,起點到各頂點最短路徑一直要到演算法結束才確定。

1.2.3 Floyd 演算法

Floyd 演算法又稱插點法,運用動態規劃思想求解有權圖中多源點之間最短路徑問題。演算法從圖的帶權鄰接矩陣開始,遞迴地進行 n 次更新得到圖的距離矩陣,進而可以得到最短路徑節點矩陣。

Floyd 演算法的時間複雜度為 O(n^3),空間複雜度為 O(n^2)。演算法時間複雜度較高,不適合計算大量資料。Floyd 演算法的優點是可以一次性求解任意兩個節點之間的最短距離,對於稠密圖的效率高於執行 V 次 Dijkstra演算法。

Floyd 演算法可以處理負權邊。

Floyd 演算法號稱只有 5行程式碼,我們來欣賞一下:

for(k=0;k<n;k++)//中轉站0~k
    for(i=0;i<n;i++) //i為起點
        for(j=0;j<n;j++) //j為終點
            if(d[i][j]>d[i][k]+d[k][j])//鬆弛操作 
                d[i][j]=d[i][k]+d[k][j]; 

1.2.4 A* 演算法

A*演算法是一種靜態路網中求解最短路徑最有效的直接搜尋方法。

A*演算法是啟發式演算法,採用最佳優先(Best-first)搜尋策略,基於估價函式對每個搜尋位置的評估結果,猜測最好的位置優先進行搜尋。

A*演算法極大地減少了低質量的搜尋路徑,因而搜尋效率很高,比傳統的路徑規劃演算法實時性更高、靈活性更強;但是,A*演算法找到的是相對最優路徑,不是絕對的最短路徑,適合大規模、實時性高的問題。

1.3 最短路徑演算法的選擇

  1. 需要求解任意兩個節點之間的最短距離,使用 Floyd 演算法;
  2. 只要求解單源最短路徑問題,有負權邊時使用 Bellman-Ford 演算法,沒有負權邊時使用 Dijkstra 演算法;
  3. A*演算法找到的是相對最優路徑,適合大規模、實時性高的問題。


2. NetworkX 中的最短路徑演算法

NetworkX 提供了豐富的最短路徑函式,除了常見的 Dijkstra 演算法、Bellman-ford 演算法、Floyd Warshall 演算法和 A*演算法,還有 Goldbery-Radzik 演算法和 Johnson 演算法。其中,Bellman-ford 演算法函式使用的是佇列改進演算法,即以 SPFA 演算法實現。

2.1 無向圖和有向圖的最短路徑求解函式

函式 功能
shortest_path(G[, source, target, weight,...]) 計算圖中的最短路徑
all_shortest_paths(G, source, target[,...]) 計算圖中所有最短的簡單路徑
shortest_path_length(G[, source, target, ...]) 計算圖中的最短路徑長度
average_shortest_path_length(G[, weight, method]) 計算平均最短路徑長度

其中,最基本的求解最短路徑函式 shortest() 和 最短路徑長度 shortest_path_length() 是 ‘dijkstra’ 演算法和 ‘bellman-ford’ 演算法的整合介面,可以通過 method='dijkstra' 選擇不同的演算法。

shortest_path(G, source=None, target=None, weight=None, method='dijkstra')
shortest_path_length(G, source=None, target=None, weight=None, method='dijkstra')

主要引數:

  • G(NetworkX graph):圖。
  • source (node):起點。
  • target (node):終點。
  • weight (string or function):引數為字串(string)時,按該字串查詢邊的屬性作為權重;如果該字串對應的邊屬性不存在,則權重置為 1;引數為函式時,邊的權重是函式的返回值。
  • method [string, optional (default = ‘dijkstra’)]:支援的選項為 ‘dijkstra’, ‘bellman-ford’。

2.2 無權圖最短路徑演算法

函式 功能
single_source_shortest_path(G, source[,cutoff]) 計算從源到所有可達節點的最短路徑
single_source_shortest_path_length(G,source) 計算從源到所有可達節點的最短路徑長度
single_target_shortest_path(G, target[,cutoff]) 計算從所有可達節點到目標的最短路徑
single_target_shortest_path_length(G,target) 計算從所有可達節點到目標的最短路徑長度
all_pairs_shortest_path(G[, cutoff]) 計算所有節點之間的最短路徑
all_pairs_shortest_path_length(G[, cutoff]) 計算所有節點之間的最短路徑長度

2.3 有權圖最短路徑演算法

函式 功能
dijkstra_path(G, source, target[, weight]) 計算從源到目標的最短加權路徑
dijkstra_path_length(G, source, target[,weight]) 計算從源到目標的最短加權路徑長度
all_pairs_dijkstra_path(G[, cutoff, weight]) 計算所有節點之間的最短加權路徑
all_pairs_dijkstra_path_length(G[, cutoff,... ]) 計算所有節點之間的最短加權路徑長度
bellman_ford_path(G, source, target[, weight]) 計算從源到目標的最短路徑
bellman_ford_path_length(G, source, target) 計算從源到目標的最短路徑長度
all_pairs_bellman_ford_path(G[, weight]) 計算所有節點之間的最短路徑
all_pairs_bellman_ford_path_length(G[,weight]) 計算所有節點之間的最短路徑長度
floyd_warshall(G[, weight]) 用 Floyd 法計算所有節點之間的最短路徑長度
floyd_warshall_numpy(G[, nodelist, weight]) 用 Floyd 法計算所有指定節點之間的最短路徑長度


3. NetworkX 中的 Dijkstra 演算法

NetworkX 中關於 Dijkstra 演算法提供了 13 個函式,很多函式的功能是重複的。這裡只介紹最基本的函式 dijkstra_path() 和 dijkstra_path_length()。

3.1 dijkstra_path() 和 dijkstra_path_length() 使用說明

dijkstra_path() 用於計算從源到目標的最短加權路徑,dijkstra_path_length() 用於計算從源到目標的最短加權路徑長度。

dijkstra_path(G, source, target, weight='weight')
dijkstra_path_length(G, source, target, weight='weight')

主要引數:

  • G(NetworkX graph):圖。
  • source (node):起點。
  • target (node):終點。
  • weight (string or function):引數為字串(string)時,按該字串查詢邊的屬性作為權重;如果該字串對應的邊屬性不存在,則權重置為1;引數為函式時,邊的權重是函式的返回值。

返回值:

  • dijkstra_path() 的返回值是最短加權路徑中的節點列表,資料型別為list。
  • dijkstra_path_length() 的返回值是最短加權路徑的長度(路徑中的邊的權重之和)。

3.2 例題 1:無向圖的最短路徑問題

例題 1:已知如圖的有權無向圖,求頂點 v1 到 頂點 v11 的最短路徑。

本問題來自:司守奎、孫兆亮,數學建模演算法與應用(第2版),P43,例4.3,國防工業出版社。

程式說明:

  1. 圖的輸入。本例的問題是稀疏的有權無向圖,使用 add_weighted_edges_from() 函式可以用列表形式向圖中新增多條賦權邊,每個賦權邊以元組 (node1,node2,weight) 表示。
  2. 圖的繪製。使用 nx.draw() 繪圖時,預設的頂點位置可能並不理想,可以通過 pos 指定頂點位置。
  3. 繪製邊的屬性。使用 nx.draw_networkx_edge_labels() 可以繪製邊的屬性,例程中選擇顯示權重屬性。
  4. 使用 dijkstra_path() 和 dijkstra_path_length() 求指定頂點之間的最短加權路徑和最短加權路徑長度。

3.3 dijkstra_path() 演算法例程

# mathmodel16_v1.py
# Demo16 of mathematical modeling algorithm
# Demo of shortest path with NetworkX
# Copyright 2021 YouCans, XUPT
# Crated:2021-07-07

import matplotlib.pyplot as plt # 匯入 Matplotlib 工具包
import networkx as nx  # 匯入 NetworkX 工具包


# 問題 1:無向圖的最短路問題(司守奎,數學建模演算法與應用,P43,例4.3)
G1 = nx.Graph()  # 建立:空的 無向圖
G1.add_weighted_edges_from([(1,2,2),(1,3,8),(1,4,1),
                            (2,3,6),(2,5,1),
                            (3,4,7),(3,5,5),(3,6,1),(3,7,2),
                            (4,7,9),
                            (5,6,3),(5,8,2),(5,9,9),
                            (6,7,4),(6,9,6),
                            (7,9,3),(7,10,1),
                            (8,9,7),(8,11,9),
                            (9,10,1),(9,11,2),
                            (10,11,4)])  # 向圖中新增多條賦權邊: (node1,node2,weight)
print('nx.info:',G1.nodes)  # 返回圖的基本資訊

# 兩個指定頂點之間的最短加權路徑
minWPath_v1_v11 = nx.dijkstra_path(G1, source=1, target=11)  # 頂點 1 到 頂點 11 的最短加權路徑
print("頂點 v1 到 頂點 v11 的最短加權路徑: ", minWPath_v1_v11)
# 兩個指定頂點之間的最短加權路徑的長度
lMinWPath_v1_v11 = nx.dijkstra_path_length(G1, source=1, target=11)  # 最短加權路徑長度
print("頂點 v1 到 頂點 v11 的最短加權路徑長度: ", lMinWPath_v1_v11)

pos = {1: (0,4), 2: (5,7), 3: (5,4), 4: (5,1), 5: (10,7), 6: (10,4), 7: (10,1),
       8: (15,7), 9: (15,4), 10: (15,1), 11: (20,4)}  # 指定頂點位置
labels = nx.get_edge_attributes(G1, 'weight')  # 設定邊的 labels 為 ‘weight'
nx.draw(G1, pos, with_labels=True, font_color='w')  # 繪製無向圖
nx.draw_networkx_edge_labels(G1, pos, edge_labels=labels, font_color='c')  # 顯示邊的權值
plt.show()

3.4 程式執行結果

nx.info: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
頂點 v1 到 頂點 v11 的最短加權路徑:  [1, 2, 5, 6, 3, 7, 10, 9, 11]
頂點 v1 到 頂點 v11 的最短加權路徑長度:  13


4. NetworkX 中的 Bellman-Ford 演算法

NetworkX 中關於 Bellman-Ford 演算法提供了多個函式,這裡只介紹最基本的函式 bellman_ford_path() 和 bellman_ford_path_length()。

4.1 bellman_ford_path() 和 bellman_ford_path_length() 使用說明

bellman_ford_path() 用於計算從源到目標的最短加權路徑,bellman_ford_path_length() 用於計算從源到目標的最短加權路徑長度。

bellman_ford_path(G, source, target, weight='weight')
bellman_ford_path_length(G, source, target, weight='weight')

主要引數:

  • G(NetworkX graph):圖。
  • source (node):起點。
  • target (node):終點。
  • weight (string):按字串查詢邊的屬性作為權重。預設值為權重 'weight'。

返回值:

  • bellman_ford_path() 的返回值是最短加權路徑中的節點列表,資料型別為list。
  • bellman_ford_path_length() 的返回值是最短加權路徑的長度(路徑中的邊的權重之和)。

4.2 例題 2:城市間機票價格問題

例題 2:城市間機票價格問題。

已知 6個城市之間的機票票價如矩陣所示(無窮大表示沒有直航),求城市 c0 到其它城市 c1...c5 的票價最便宜的路徑及票價。

\[\begin{bmatrix} 0 & 50 & \infty & 40 & 25 & 10\\ 50 & 0 & 15 & 20 & \infty & 25\\ \infty & 15 & 0 & 10 & 20 & \infty\\ 40 & 20 & 10 & 0 & 10 & 25\\ 25 & \infty & 20 & 10 & 0 & 55\\ 10 & 25 & \infty & 25 & 55 & 0\\ \end{bmatrix} \]

本案例問題改編自:司守奎、孫兆亮,數學建模演算法與應用(第2版),P41,例4.1,國防工業出版社。

程式說明

  1. 圖的輸入。使用 pandas 中 DataFrame 讀取資料檔案非常方便,本例中以 pandas 輸入頂點鄰接矩陣,使用 nx.from_pandas_adjacency(dfAdj) 轉換為 NetworkX 的圖。
  2. 鄰接矩陣。鄰接矩陣 dfAdj (i,j) 的值表示連線頂點 i、j 的邊的權值, dfAdj (i,j) = 0 表示 i、j 不相鄰, 本例中表示沒有直航。
  3. 最短路徑與最短路徑長度。nx.shortest_path() 返回最短路徑。nx.shortest_path_length() 返回最短路徑長度,本例中可以理解為從起點到終點的乘機次數:1 表示直航,2 表示中轉一次。
  4. 最短加權路徑長度。nx.bellman_ford_path_length() 返回最短加權路徑長度,本例中權重為票價,最短加權路徑長度即為兩點間最便宜的直航或中轉的機票票價。
    通過本案例,可以直觀地理解最短路徑長度與最短加權路徑長度的區別。

4.3 bellman_ford_path() 演算法例程

# mathmodel16_v1.py
# Demo16 of mathematical modeling algorithm
# Demo of shortest path with NetworkX
# Copyright 2021 YouCans, XUPT
# Crated:2021-07-07

import pandas as pd
import matplotlib.pyplot as plt # 匯入 Matplotlib 工具包
import networkx as nx  # 匯入 NetworkX 工具包

# 問題 2:城市間機票價格問題(司守奎,數學建模演算法與應用,P41,例4.1)
# # 從Pandas資料格式(頂點鄰接矩陣)建立 NetworkX 圖
# # from_pandas_adjacency(df, create_using=None) # 鄰接矩陣,n行*n列,矩陣資料表示權重
dfAdj = pd.DataFrame([[0, 50, 0, 40, 25, 10],  # 0 表示不鄰接,
                      [50, 0, 15, 20, 0, 25],
                      [0, 15, 0, 10, 20, 0],
                      [40, 20, 10, 0, 10, 25],
                      [25, 0, 20, 10, 0 ,55],
                      [10, 25, 0, 25, 55, 0]])
G2 = nx.from_pandas_adjacency(dfAdj)  # 由 pandas 頂點鄰接矩陣 建立 NetworkX 圖

# 計算最短路徑:注意最短路徑與最短加權路徑的不同
# 兩個指定頂點之間的最短路徑
minPath03 = nx.shortest_path(G2, source=0, target=3)  # 頂點 0 到 頂點 3 的最短路徑
lMinPath03 = nx.shortest_path_length(G2, source=0, target=3)  #最短路徑長度
print("頂點 0 到 3 的最短路徑為:{},最短路徑長度為:{}".format(minPath03, lMinPath03))
# 兩個指定頂點之間的最短加權路徑
minWPath03 = nx.bellman_ford_path(G2, source=0, target=3)  # 頂點 0 到 頂點 3 的最短加權路徑
# 兩個指定頂點之間的最短加權路徑的長度
lMinWPath03 = nx.bellman_ford_path_length(G2, source=0, target=3)  #最短加權路徑長度
print("頂點 0 到 3 的最短加權路徑為:{},最短加權路徑長度為:{}".format(minWPath03, lMinWPath03))

for i in range(1,6):
    minWPath0 = nx.bellman_ford_path(G2, source=0, target=i)  # 頂點 0 到其它頂點的最短加權路徑
    lMinPath0 = nx.bellman_ford_path_length(G2, source=0, target=i)  #最短加權路徑長度
    print("城市 0 到 城市 {} 機票票價最低的路線為: {},票價總和為:{}".format(i, minWPath0, lMinPath0))

nx.draw_shell(G2, with_labels=True, node_color='r', edge_color='b', font_color='w', width=2)
plt.show()

4.4 程式執行結果

頂點 0 到 3 的最短路徑為:[0, 3],最短路徑長度為:1
頂點 0 到 3 的最短加權路徑為:[0, 4, 3],最短加權路徑長度為:35
城市 0 到 城市 1 機票票價最低的路線為: [0, 5, 1],票價總和為:35
城市 0 到 城市 2 機票票價最低的路線為: [0, 4, 2],票價總和為:45
城市 0 到 城市 3 機票票價最低的路線為: [0, 5, 3],票價總和為:35
城市 0 到 城市 4 機票票價最低的路線為: [0, 4],票價總和為:25
城市 0 到 城市 5 機票票價最低的路線為: [0, 5],票價總和為:10


5. 總結

  1. 最短路徑問題是圖論研究中的經典演算法問題,用於計算圖中一個頂點到另一個頂點的最短路徑。
  2. 在圖論中,求最短路徑長度是要計算從起點到終點步數最少的路徑,而求最短路徑距離是要計算最短加權路徑長度。從例 2的執行結果可以對比二者的區別。
  3. 求最短路徑長度的常用演算法是 Dijkstra 演算法、Bellman-Ford 演算法和Floyd 演算法,另外還有啟發式演算法 A*。
  4. 對於稀疏的圖推薦使用列表形式新增賦權邊,對於稠密圖、完全圖建議使用 DtaFrame 讀取資料檔案後再轉換為 NetworkX 格式。

【本節完】


版權宣告:

歡迎關注『Python小白的數學建模課 @ Youcans』 原創作品

原創作品,轉載必須標註原文連結:(https://www.cnblogs.com/youcans/p/15107067.html)。

Copyright 2021 Youcans, XUPT

Crated:2021-07-07


歡迎關注 『Python小白的數學建模課 @ Youcans』,每週更新數模筆記
Python小白的數學建模課-01.新手必讀
Python小白的數學建模課-02.資料匯入
Python小白的數學建模課-03.線性規劃
Python小白的數學建模課-04.整數規劃
Python小白的數學建模課-05.0-1規劃
Python小白的數學建模課-06.固定費用問題
Python小白的數學建模課-07.選址問題
Python小白的數學建模課-09.微分方程模型
Python小白的數學建模課-10.微分方程邊值問題
Python小白的數學建模課-12.非線性規劃
Python小白的數學建模課-15.圖論的基本概念
Python小白的數學建模課-B2.新冠疫情 SI模型
Python小白的數學建模課-B3.新冠疫情 SIS模型
Python小白的數學建模課-B4.新冠疫情 SIR模型
Python小白的數學建模課-B5.新冠疫情 SEIR模型
Python小白的數學建模課-B6.改進 SEIR疫情模型


相關文章