Python數模筆記-NetworkX(3)條件最短路徑

youcans發表於2021-05-21

1、帶有條件約束的最短路徑問題

最短路徑問題是圖論中求兩個頂點之間的最短路徑問題,通常是求最短加權路徑。

條件最短路徑,指帶有約束條件、限制條件的最短路徑。例如,頂點約束,包括必經點或禁止點的限制;邊的約束,包括必經路段或禁止路段;還包括無權路徑長度的限制,即經過幾步到達終點。進一步地,還有雙目標限制的最短路徑問題,求最短距離中花費最小的路線;交通限制條件下的最短路徑問題,需要考慮轉向限制和延誤的約束。

求解帶有限制條件的最短路徑問題,總體來說可以分為兩類基本方法:一類是基於不帶限制條件的最短路徑演算法,對求解過程中的每一條有效路徑,都用限制條件進行判斷,如果滿足所有限制條件則繼續,如果不滿足限制條件則放棄該路徑;另一類方法是基於具體問題和選擇演算法的特點,將問題轉化為有約束的規劃問題來處理。

但是,如果使用 NetworkX 求解帶有限制條件的最短路徑問題,採用這兩類方法都會有一些困難。原因在於前文所介紹的 NetworkX 提供的 Dijkstra 演算法、Bellman-Ford 演算法、Floyd 演算法和啟發式演算法 A* 都是封裝函式,沒有提供設定約束條件的選項和介面,因此使用者不能把條件判斷語句加入這些封裝函式的程式內部。這種問題不僅存在於 Python 語言的 Network 工具包,對於其它計算機語言的工具包也是類似的:自己程式設計序費時費力,但可以根據需要修改和擴充套件;直接呼叫工具包的演算法函式非常方便,但不能進行修改或擴充套件。

不過,NetworkX 可以生成兩個頂點之間的所有簡單路徑,而且可以獲得所有簡單路徑的邊的列表。利用簡單路徑演算法,可以通過對約束條件的判斷來求解帶有頂點約束和邊約束的最短路徑問題。


歡迎關注 Youcans 原創系列,每週更新數模筆記

Python數模筆記-PuLP庫
Python數模筆記-StatsModels統計迴歸
Python數模筆記-Sklearn
Python數模筆記-NetworkX
Python數模筆記-模擬退火演算法


2、問題案例:螞蟻的最優路徑分析

蟻巢有若干個儲藏間(圖中圓圈表示),儲藏間之間有路徑相連(路徑拓撲結構如圖所示)。該圖為無向圖,路徑通行的花費如圖中線路上的數字所示,路徑正反方向通行的花費相同。要求從起點 N0 到終點 N17 的最優路徑,並需要滿足限制條件:

  • 需要儘可能以最少的花費到達終點;
  • 必須經過圖中的綠色節點;
  • 必須經過圖中的兩段綠色路段;
  • 必須避開圖中的紅色路段。

說明:本案例出自西安郵電大學第12屆數學建模競賽賽題,本文進行了改編。


3、NetworkX 求解帶有條件約束的最短路徑問題

3.1 圖的建立和視覺化

Python 例程(NetworkX)

# networkX_E3.py
# Demo of shortest path with NetworkX
# Copyright 2021 YouCans, XUPT
# Crated:2021-05-20

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

# 問題 1:螞蟻的最優路徑分析(西安郵電大學第12屆數學建模競賽B題)

gAnt = nx.Graph()  # 建立:空的 無向圖
gAnt.add_weighted_edges_from([(0,1,3),(0,2,1),(0,3,1),
                            (1,2,1),(1,4,1),(1,9,4),
                            (2,3,1),(2,4,2),(2,5,1),
                            (3,5,2),(3,6,2),(3,7,1),
                            (4,5,1),(4,9,1),
                            (5,6,1),(5,9,3),(5,10,1),(5,12,3),
                            (6,7,1),(6,8,2),(6,12,2),(6,13,4),(6,14,3),
                            (7,8,1),
                            (8,14,1),(8,15,3),
                            (9,10,1),(9,11,1),
                            (10,11,1),(10,12,2),
                            (11,12,1),(11,16,1),
                            (12,13,2),(12,16,1),
                            (13,14,1),(13,15,2),(13,16,2),(13,17,1),
                            (14,15,1),
                            (15,17,4),
                            (16,17,1)])  # 向圖中新增多條賦權邊: (node1,node2,weight)

pos={0:(1,8),1:(4,12),2:(4,9),3:(4,6),4:(8,11),5:(9,8),  # 指定頂點位置
     6:(11,6),7:(8,4),8:(12,2),9:(12,13),10:(15,11),11:(18,13),
     12:(19,9),13:(22,6),14:(18,4),15:(21,2),16:(22,11),17:(28,8)}
nx.draw(gAnt, pos, with_labels=True, alpha=0.8)
labels = nx.get_edge_attributes(gAnt,'weight')
nx.draw_networkx_edge_labels(gAnt,pos,edge_labels=labels, font_color='c') # 顯示權值
nx.draw_networkx_nodes(gAnt,pos,nodelist=[0,17],node_color='yellow')  # 設定頂點顏色
nx.draw_networkx_nodes(gAnt,pos,nodelist=[7,12],node_color='lime')  # 設定頂點顏色
nx.draw_networkx_edges(gAnt,pos,edgelist=[(2,4),(13,14)],edge_color='lime',width=2.5)  # 設定邊的顏色
nx.draw_networkx_edges(gAnt,pos,edgelist=[(11,12)],edge_color='r',width=2.5)  # 設定邊的顏色
plt.show()

執行結果

本段程式繪製網路圖,包括頂點、邊、邊的權值,特殊頂點和特殊邊的顏色設定。

Python數模筆記-NetworkX(3)條件最短路徑

程式說明

  1. 圖的建立。本例使用 nx.Graph() 建立無向圖,然後用 gAnt.add_weighted_edges_from() 函式以列表向圖中新增多條賦權邊,每個賦權邊以元組 (node1,node2,weight) 表示。
  2. 圖的繪製。使用nx.draw()繪圖時,預設的節點位置並不理想,可以使用 pos 屬性引數指定節點位置。pos 為字典資料型別,按 node:(x_pos,y_pos) 格式設定節點位置。
  3. 顯示邊的權值。使用 nx.draw_networkx_edge_labels() 可以繪製邊的屬性,本例中選擇顯示權值屬性。
  4. 設定頂點屬性。nx.draw_networkx_nodes() 可以設定頂點的屬性,例如對 nodelist 列表中的節點設定顏色屬性 node_color。
  5. 設定邊的屬性。nx.draw_networkx_edges() 可以設定邊的屬性,例如對 edgelist 列表中的邊設定線寬屬性 width 和顏色屬性 edge_color。

3.2 無限制條件的最短路徑

程式說明

  1. 對於無限制條件的最短路徑問題,NetworkX 提供了 Dijkstra 演算法、Bellman-Ford 演算法、Floyd 演算法和啟發式演算法 A* 的函式。
  2. 例程使用 nx.dijkstra_path() 和 nx.dijkstra_path_length() 呼叫 Dijkstra 演算法求兩個指定頂點之間的最短加權路徑和最短加權路徑長度。

Python 例程(NetworkX)

# 兩個指定頂點之間的最短加權路徑
minWPath1 = nx.dijkstra_path(gAnt, source=0, target=17)  # 頂點 0 到 頂點 17 的最短加權路徑
# 兩個指定頂點之間的最短加權路徑的長度
lMinWPath1 = nx.dijkstra_path_length(gAnt, source=0, target=17)  #最短加權路徑長度
print("\n問題1: 無限制條件")
print("S 到 E 的最短加權路徑: ", minWPath1)
print("S 到 E 的最短加權路徑長度: ", lMinWPath1)

執行結果

問題1: 無限制條件
S 到 E 的最短加權路徑:  [0, 2, 5, 10, 11, 16, 17]
S 到 E 的最短加權路徑長度:  6

3.3 限制條件:禁止點或禁止邊

程式說明

  1. 禁止點或者禁止邊的處理比較簡單,從圖中刪除對應的禁止頂點或禁止邊即可。當然,在建立圖時就不新增這些頂點和邊更簡單,但這樣在繪圖時也無法反映這些頂點和邊。
  2. 使用 remove_node(n) 刪除指定頂點 n,remove_edge(u,v) 刪除指定的邊 (u,v)。
  3. 使用 remove_nodes_from([n1,...nk]) 刪除多個頂點,remove_edges_from([(u1,v1),...(uk,vk)]) 刪除多條邊。
  4. 例程中刪除的點和邊與案例問題中的要求不一致,是為了示例刪除函式的使用。下同。

Python 例程

# 2. 限制條件:禁止點或禁止邊
# 解決方案:從圖中刪除禁止頂點或禁止邊
gAnt.remove_nodes_from([5])  # 通過頂點標籤 5 刪除頂點
gAnt.remove_edge(13,17)  # 刪除邊 (13,17)
minWPath2 = nx.dijkstra_path(gAnt, source=0, target=17)  # 頂點 0 到 頂點 17 的最短加權路徑
lMinWPath2 = nx.dijkstra_path_length(gAnt, source=0, target=17)  #最短加權路徑長度
print("\n問題2: 禁止點或禁止邊的約束")
print("S 到 E 的最短加權路徑: ", minWPath2)
print("S 到 E 的最短加權路徑長度: ", lMinWPath2)

執行結果

問題2: 禁止點或禁止邊的約束
S 到 E 的最短加權路徑:  [0, 3, 6, 12, 16, 17]
S 到 E 的最短加權路徑長度:  7

3.4 限制條件:一個必經點

程式說明

  1. 當限制條件為一個必經點時,可以把原問題分解為兩個子問題:子問題 1 為起點至必經點,子問題 2 為必經點至終點。
  2. 對兩個子問題分別用 Dijkstra 演算法求最短加權路徑和最短加權路徑長度,然後進行合併,就得到經過必經點的原問題的最短加權路徑和最短加權路徑長度。

Python 例程

# 3. 限制條件:一個必經點
# 解決方案:分解為兩個問題,問題 1 為起點N0至必經點N6,問題 2 為必經點N6至終點N17
minWPath3a = nx.dijkstra_path(gAnt, source=0, target=6)  # N0 到 N6 的最短加權路徑
lMinWPath3a = nx.dijkstra_path_length(gAnt, source=0, target=6)  # 最短加權路徑長度
minWPath3b = nx.dijkstra_path(gAnt, source=6, target=17)  # N6 到 N17 的最短加權路徑
lMinWPath3b = nx.dijkstra_path_length(gAnt, source=6, target=17)  # 最短加權路徑長度
minWPath3a.extend(minWPath3b[1:]) # 拼接 minWPath3a、minWPath3b 並去重 N7
print("\n問題3: 一個必經點的約束")
print("S 到 E 的最短加權路徑: ", minWPath3a)
print("S 到 E 的最短加權路徑長度: ", lMinWPath3a+lMinWPath3b)

執行結果

問題3: 一個必經點的約束
S 到 E 的最短加權路徑:  [0, 3, 6, 12, 16, 17]
S 到 E 的最短加權路徑長度:  7

3.5 限制條件:多個必經點(方案一)

程式說明

  1. 當限制條件為兩個或多個必經點時,起點、終點與各必經點的次序並不確定,即從起點出發就不知道應該先去哪一個必經點,因此不宜再用分段求最小路徑的方法處理。
  2. NetworkX 提供了 all_simple_paths() 函式,可以生成兩個頂點之間的所有簡單路徑。利用簡單路徑演算法,可以通過對約束條件的判斷來求解帶有多個頂點約束的最短路徑問題。
  3. 程式實現的步驟包括:(1)遍歷起點為0、終點為17的簡單路徑;(2)檢查路徑是否滿足包括頂點 N7、N15 的限制條件;(3)在滿足限制條件的簡單路徑中找加權長度最短的路徑;(4)求最短路徑的加權路徑長度。
  4. 本段例程非常簡練,綜合使用了幾種 Python 語言迴圈、判斷結構的簡潔寫法,需要逐步分析。

Python 例程

# 4. 限制條件:多個必經點 (N7,N15)
# 解決方案:遍歷從起點到終點的簡單路徑,求滿足必經點條件的最短路徑
minWPath4 = min([path  # 返回 key 為最小值的 path
    for path in nx.all_simple_paths(gAnt, 0, 17)  # gAnt 中所有起點為0、終點為17的簡單路徑
    if all(n in path for n in (7, 15))], # 滿足路徑中包括頂點 N7,N15
    key=lambda x: sum(gAnt.edges[edge]['weight'] for edge in nx.utils.pairwise(x))) # key 為加權路徑長度
lenPath = sum(gAnt.edges[edge]['weight'] for edge in nx.utils.pairwise(minWPath4))  # 求指定路徑的加權路徑長度
print("\n問題4: 多個必經點的約束")
print("S 到 E 的最短加權路徑: ", minWPath4)
print("S 到 E 的最短加權路徑長度: ", lenPath)

執行結果

問題4: 多個必經點的約束
S 到 E 的最短加權路徑:  [0, 3, 7, 8, 14, 15, 13, 17]
S 到 E 的最短加權路徑長度:  8

3.6 限制條件:多個必經點(方案二)

程式說明

  1. 本例與 3.5 的問題實際上是相同的。限制條件都是多個必經頂點 N7、N15,解決方案都是使用 all_simple_paths() 函式生成兩個頂點間的所有簡單路徑,程式實現的步驟也是類似的。
  2. 本方案按照典型的迴圈、判斷結構的寫法,便於閱讀和理解。此外,如果還有其它約束條件或子任務需要在迴圈中處理,這樣的結構更容易實現。

Python 例程

# 5. 限制條件:多個必經點 (N7,N15)
# 解決方案:遍歷從起點到終點的簡單路徑,求滿足必經點條件的最短路徑
lMinWPath5 = minWPath5 = 1e9
for path in nx.all_simple_paths(gAnt, 0, 17):
    if all(n in path for n in (7,15)):  # 滿足路徑中包括頂點 N7,N15
        lenPath = sum(gAnt.edges[edge]['weight'] for edge in nx.utils.pairwise(path))
        if lenPath < lMinWPath5:
            lMinWPath5 = lenPath
            minWPath5 = path
print("\n問題5: 多個必經點的約束")
print("S 到 E 的最短加權路徑: ", minWPath5)
print("S 到 E 的最短加權路徑長度: ", lMinWPath5)

執行結果

問題5: 多個必經點的約束
S 到 E 的最短加權路徑:  [0, 3, 7, 8, 14, 15, 13, 17]
S 到 E 的最短加權路徑長度:  8

3.7 限制條件:必經邊

程式說明

  1. 必經點的處理,實際上還可以有更好的方法,其思想是結合 Dijkstra 演算法的實現過程, 將限制條件作為縮小搜尋空間的條件,可以降低演算法的複雜度。但對於多個必經邊來說,很難以此來改進基礎的無約束演算法,通常的處理方法就是在演算法中增加一個判斷是否滿足約束條件的過程。
  2. 本例仍然延續處理多個必經點的思路,利用簡單路徑演算法,可以通過對約束條件的判斷來求解帶有多個必經邊約束條件的最短路徑問題,可以同時處理必經點約束條件。
  3. 本例程對應案例中的各項約束條件: 必須經過圖中的綠色節點;必須經過圖中的兩段綠色路段;必須避開圖中的紅色路段;儘可能以最少的花費到達終點。
  4. 本例程的框架和步驟同 3.6,這是一個遍歷簡單路徑、判斷約束條件的通用框架。
  5. all(n in path for n in (2,4,7,12,13,14)) 的作用,一是判斷路徑中是否包括必經點 N7、N12;二是判斷路徑中是否包括必經邊 (2,4)、(13,14) 的各頂點,這不僅可以減小計算量,而且能確保下面使用 index() 查詢頂點位置時不會發生錯誤。

Python 例程

# 6. 限制條件:必經邊 (N2,N4), (N13,N14),必經點 N7,N12
# 解決方案:遍歷從起點到終點的簡單路徑,求滿足必經邊條件的最短路徑
gAnt.remove_edge(11,12)  # 禁止邊 (11,12)
lMinWPath6 = minWPath6 = 1e9  # 置初值
for path in nx.all_simple_paths(gAnt, 0, 17):  # 所有起點為0、終點為17的簡單路徑
    if all(n in path for n in (2,4,7,12,13,14)): # 滿足路徑中包括頂點 N7,N12
        # 檢查 (N2,N4)
        p1 = path.index(2)  # N2 的位置
        if (path[p1-1]!=4 and path[p1+1]!=4): continue  # 判斷 N2~N4 是否相鄰
        # 檢查 (N13,N14)
        p2 = path.index(13)  # # N13 的位置
        if (path[p2-1]!=14 and path[p2+1]!=14): continue  # 判斷 N13~N14 是否相鄰
        lenPath = sum(gAnt.edges[edge]['weight'] for edge in nx.utils.pairwise(path))
        if lenPath < lMinWPath6:
            lMinWPath6 = lenPath
            minWPath6 = path

print("\n問題6: 多個必經邊、必經點的約束")
print("S 到 E 的最短加權路徑: ", minWPath6)
print("S 到 E 的最短加權路徑長度: ", lMinWPath6)

edgeList = []
for i in range(len(minWPath6)-1):
    edgeList.append((minWPath6[i],minWPath6[i+1]))
nx.draw_networkx_edges(gAnt,pos,edgelist=edgeList,edge_color='m',width=4)  # 設定邊的顏色
plt.show()  # YouCans, XUPT

執行結果

問題6: 多個必經邊、必經點的約束
S 到 E 的最短加權路徑:  [0, 2, 4, 5, 6, 7, 8, 14, 13, 12, 16, 17]
S 到 E 的最短加權路徑長度:  13
Python數模筆記-NetworkX(3)條件最短路徑

版權說明:

本文內容及例程為作者原創,並非轉載書籍或網路內容。
本文中案例問題來自:

YouCans 原創作品,如轉載需標註原始連結。
Copyright 2021 YouCans, XUPT
Crated:2021-05-20


歡迎關注 Youcans 原創系列,每週更新數模筆記

Python數模筆記-PuLP庫(1)線性規劃入門
Python數模筆記-PuLP庫(2)線性規劃進階
Python數模筆記-PuLP庫(3)線性規劃例項
Python數模筆記-Scipy庫(1)線性規劃問題
Python數模筆記-StatsModels 統計迴歸(1)簡介
Python數模筆記-StatsModels 統計迴歸(2)線性迴歸
Python數模筆記-StatsModels 統計迴歸(3)模型資料的準備
Python數模筆記-StatsModels 統計迴歸(4)視覺化
Python數模筆記-Sklearn (1)介紹
Python數模筆記-Sklearn (2)聚類分析
Python數模筆記-Sklearn (3)主成分分析
Python數模筆記-Sklearn (4)線性迴歸
Python數模筆記-Sklearn (5)支援向量機
Python數模筆記-NetworkX(1)圖的操作
Python數模筆記-NetworkX(2)最短路徑
Python數模筆記-NetworkX(3)條件最短路徑
Python數模筆記-模擬退火演算法(1)多變數函式優化
Python數模筆記-模擬退火演算法(2)約束條件的處理
Python數模筆記-模擬退火演算法(3)整數規劃問題
Python數模筆記-模擬退火演算法(4)旅行商問題


相關文章