漢米爾頓路徑問題 (III) - 使用 Google OR-Tools 實作
===
###### tags: `TSP` `Hamiltonian Path` `Operations Research` `OR-Tools` `Python`
## 前言
[__上一篇文章__](https://hackmd.io/kdviO126QJq6B4YfxrGzwQ) 說明了最短漢米爾頓路徑問題的數學模式,這篇則將說明如何撰寫程式,把最短漢米爾頓路徑問題實作出來。
本文實作將使用 Python 程式語言,並使用 Google 開發的 OR-Tools 套件求解。本文將提供三種方式求解最短漢米爾頓路徑。
## 問題描述
小乖是一個咖哩愛好者,有一天他搭高鐵到台北,決定要來品嘗一下台北好吃的咖哩。他在清單上列出了 6 家咖哩店,預計從台北車站出發,造訪完清單上所有咖哩店後,最後到大稻埕碼頭貨櫃市集喝酒,結束一日台北咖哩行。因為時間有限,小乖希望可以用最短旅行時間跑完上述的行程。
上述問題即是最短漢米爾頓路徑問題,給定起點 (台北車站) 和訖點 (大稻埕碼頭貨櫃市集),求出一條經過所有節點且為距離或旅行時間為最短的路徑,也就是:
```
台北車站 -> ? -> ? -> ? -> ? -> ? -> ? -> 大稻埕碼頭貨櫃市集
```
我們要做的就是把中間這些點做排序,使這條路徑為最短。
:::danger
:warning: __聲明:本文絕對沒有業配!沒有業配!沒有業配!__ (但如果要找我業配也是可以^^)
:::
以下為起訖點及 6 家咖哩店的經緯度座標:
|編號|地點|經度|緯度|
|:--:|:--:|:--:|:--:|
|0|台北車站 (起點)|121.51705498294422|25.04812699185495|
|1|溫咖哩|121.52694765430479|25.04160674979448|
|2|哞屋|121.52294339950605|25.064214083600284|
|3|勝利洋食|121.54956207412458|25.043419518468635|
|4|天菜咖哩|121.55331801759017|25.043348130259034|
|5|島津和牛咖哩民生店|121.55186587878282|25.05846636112716|
|6|新高軒信義店(A11-B2)|121.5672858678217|25.03670073623688|
|7|大稻埕碼頭貨櫃市集 (訖點)|121.50742317199105|25.0561805065471|
<br>
用地圖呈現點位分布如下圖,粉紅色表示起點,橘色表示終點:

<br>
## 取得旅行時間矩陣
這裡我們透過 [Open Street Routing Machine (OSRM)](https://project-osrm.org/) 取得旅行時間矩陣,可以利用筆者開發的 [OSRM API](https://github.com/chhuang17/osrm_api) 取得相關資料。
```python=
from osrm_api import osrm
startPoint = (121.51705498294422, 25.04812699185495) # 台北車站
endPoint = (121.50742317199105, 25.0561805065471) # 大稻埕碼頭貨櫃市集
# Curry shop
curryShops = [
(121.52694765430479, 25.04160674979448), # 溫咖哩
(121.52294339950605, 25.064214083600284), # 哞屋Mon wo 焼き肉丼 、咖哩カレー
(121.54956207412458, 25.043419518468635), # 勝利洋食
(121.55331801759017, 25.043348130259034), # 天菜咖哩
(121.55186587878282, 25.05846636112716), # 島津和牛咖哩民生店
(121.5672858678217, 25.03670073623688), # 新高軒信義店(A11-B2)
]
# 點位經緯度列表
nodeList = [startPoint] + curryShops + [endPoint]
# 點位編號
nodeNumbers = [i for i in range(len(nodeList))]
# 透過 OSRM API 取得旅行時間矩陣
timeMatx = osrm.odMatrix(nodeList, get='duration', timeUnit='second')
print(timeMatx)
# Output
# [[ 0. 188.7 330.1 330.3 375.1 510.2 492.9 261.1]
# [171.2 0. 359.2 259.3 304.1 450.8 421.9 322.4]
# [256.4 356.9 0. 494.6 539.4 417. 657.2 299.3]
# [354.2 292.3 463.8 0. 122.9 248.8 272.3 496.4]
# [368.6 306.7 478.2 61.3 0. 269.2 230. 451.9]
# [484.4 465.4 376. 303.2 289.4 0. 408.3 534.3]
# [491. 468.3 621.3 259.6 235. 435.9 0. 568.2]
# [298.2 409. 575.9 558. 536.8 693.1 640. 0. ]]
```
## 方法一:使用 MPSolver 求解
我們知道最短漢米爾頓路徑問題是一個整數規劃問題,而 OR-Tools 中的 MPSolver 可以用來解決線性規劃和混合整數規劃問題,因此當然可以用 MPSolver 求解最短漢米爾頓路徑。
### 求解器和決策變數
首先建立求解器,這裡使用 `'SCIP'` 求解器,同時建立決策變數 $x_{00},\space x_{01},\cdots, x_{77}$。我們利用 `solver.IntVar()` 建立整數決策變數,第一個 args 為決策變數下界,第二個 args 為決策變數上界,第三個 args 為變數名稱。
```python=
from ortools.linear_solver import pywraplp
# 使用 SCIP 求解器
solver = pywraplp.Solver.CreateSolver('SCIP')
# 建立二元決策變數,xij = {0,1}
variables = [solver.IntVar(0, 1, f"x{i:02d}_{j:02d}") for i in range(len(nodeNumbers)) for j in range(len(nodeNumbers))]
```
:::info
:bulb: 為了方便 debug,這裡將變數 $x_{01}$ 命名為 `x_00_01`,變數 $x_{46}$ 命名為 `x_04_06`。
:::
接著建立成本矩陣 $c_{ij}$,也就是旅行時間矩陣,並且對旅行時間矩陣稍作修改,在對角線的地方填入一個很大的數作為懲罰項,避免運算時選到自己。
```python=
c = timeMatx
# Add penalty term at cii
for i in range(c.shape[0]):
c[i][i] = 1e7
```
### 目標式
目標式為最小化總成本,這裡的總成本就是總旅行時間,即:
$$
\min \space\space \sum_{i}\sum_{j}c_{ij}x_{ij}
$$
首先建立 `objective` 物件,並針對每個變數設定其係數 (也就是 $c_{ij}$ ),最後設定求解目標為最小化。
:::info
:bulb: 目標式係數一定要 `int`,不可以是 `float`, `double`, ...。
:bulb: `var` 的資料型態是 ortools 的 Variable, 訪問它的話要先轉換成 str。
:::
```python=
objective = solver.Objective()
for var in variables:
coeff = int(round(c[int(str(var)[1:3])][int(str(var)[4:6])], 0))
objective.SetCoefficient(var, coeff)
objective.SetMinimization()
```
### 流量限制式 1 - 除了終點,其餘節點流出的總和為 1
假設共有 n 個點 (包括起點和終點),起點編號為 $1$,終點的編號為 $n$,則將此流量限制式表示成數學式如下:
$$
\begin{align}
\sum_{i}x_{ij} &= 1 \\
x_{ij} &\in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
nodeNumList = [f"{x:02d}" for x in range(len(nodeNumbers)-1)]
for node in nodeNumList:
constraint = solver.Constraint(1, 1)
for var in variables:
if str(var)[1:3] == node:
constraint.SetCoefficient(var, 1)
```
### 流量限制式 2 - 除了起點, 其餘節點流入的總和為 1
假設共有 n 個點 (包括起點和終點),起點編號為 $1$,終點的編號為 $n$,則將此流量限制式表示成數學式如下:
$$
\begin{align}
\sum_{i}x_{ij} &= 1 \\
x_{ij} &\in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
nodeNumList = [f"{x:02d}" for x in range(1, len(nodeNumbers))]
for node in nodeNumList:
constraint = solver.Constraint(1, 1)
for var in variables:
if str(var)[4:6] == node:
constraint.SetCoefficient(var, 1)
```
### 流量限制式 3 - 流入起點的總和為 0
假設共有 n 個點 (包括起點和終點),起點編號為 $1$,終點的編號為 $n$,則將此流量限制式表示成數學式如下:
$$
\begin{align}
\sum_{i}x_{i1} &= 0 \\
x_{ij} &\in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
constraint = solver.Constraint(0, 0)
for var in variables:
if str(var)[4:6] == "00":
constraint.SetCoefficient(var, 1)
```
### 流量限制式 4 - 流出終點的總和為 0
假設共有 n 個點 (包括起點和終點),起點編號為 $1$,終點的編號為 $n$,則將此流量限制式表示成數學式如下:
$$
\begin{align}
\sum_{j}x_{nj} &= 0 \\
x_{ij} &\in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
constraint = solver.Constraint(0, 0)
for var in variables:
if str(var)[1:3] == f"{len(nodeNumbers)-1:02d}":
constraint.SetCoefficient(var, 1)
```
### 子迴圈限制式
考量實作的方便性,這裡使用 MTZ 限制式消除子迴圈。MTZ 限制式的數學式為:
$$
\begin{align}
&u_{i} - u_{j} + nx_{ij} \leq n - 1 \\
&u_{i}, u_{j} \geq 0, \space\space\forall i,j = 1,2,\cdots n, \space\space\forall i \neq j \\
&x_{ij} \in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
uVars = [solver.IntVar(0, solver.infinity(), f"u{i:02d}") for i in range(len(nodeNumbers))]
for i, ui in enumerate(uVars):
for j, uj in enumerate(uVars):
for xij in variables:
if i != j and str(ui)[1:3] == str(xij)[1:3] and str(uj)[1:3] == str(xij)[4:6]:
constraint = solver.Constraint(-solver.infinity(), len(nodeNumbers)-1)
constraint.SetCoefficient(ui, 1)
constraint.SetCoefficient(uj, -1)
constraint.SetCoefficient(xij, len(nodeNumbers))
```
<br>
:::info
:bulb: 執行以下程式碼會將模式以數學規劃形式印出
:::
```python=
print(solver.ExportModelAsLpFormat(False).replace('\\', '').replace(',_', ','), sep='\n')
```
### 模式求解
呼叫 `solver.Solve()` 函式求解,並選取求解後 >0 的決策變數,表示該兩點間存在一條可行路徑。
程式碼實作如下:
```python=
solver.Solve()
odPairs = []
for xij in variables:
if xij.solution_value() > 0:
odPairs.append((int(str(xij)[1:3]), int(str(xij)[4:6])))
```
接著要將 `odPairs` 轉換成路徑。`odPairs` 的長相大概會長這樣:
```python=
# odPairs 長相, 意思是可行路徑分別為 0->4, 1->6, 2->5, ... , 3->7
odPairs = [(0, 4), (1, 6), (2, 5), ... , (3, 7)]
```
我們要做的是將這些起訖對串成一條路徑 `route`,變成 $0-4-1-6-\cdots-3-7$,下面的程式碼即是做這樣的處理,最後將回傳 `orderList`。
```python=
route = [odPairs[0][0], odPairs[0][1]]
odPairs.remove(odPairs[0])
while True:
if len(odPairs) < 1:
break
else:
for pair in odPairs:
if pair[0] in route:
route.append(pair[1])
odPairs.remove(pair)
orderList = [nodeNumbers[idx] for idx in route]
```
### 完整程式碼
```python=
from ortools.linear_solver import pywraplp
def getHamtPath(timeMatx, nodeNumbers):
""" 使用 SCIP 求解器求解最短漢米爾頓路徑 """
solver = pywraplp.Solver.CreateSolver('SCIP')
variables = [solver.IntVar(0, 1, f"x{i:02d}_{j:02d}") for i in range(len(nodeNumbers)) for j in range(len(nodeNumbers))]
c = timeMatx
for i in range(c.shape[0]):
c[i][i] = 1e7
# 目標式 - 最小化總旅行時間
objective = solver.Objective()
for var in variables:
coeff = int(round(c[int(str(var)[1:3])][int(str(var)[4:6])], 0))
objective.SetCoefficient(var, coeff)
objective.SetMinimization()
# 流量限制式 1 - 除了終點,其餘節點流出的總和為 1
nodeNumList = [f"{x:02d}" for x in range(len(nodeNumbers)-1)]
for node in nodeNumList:
constraint = solver.Constraint(1, 1)
for var in variables:
if str(var)[1:3] == node:
constraint.SetCoefficient(var, 1)
# 流量限制式 2 - 除了起點, 其餘節點流入的總和為 1
nodeNumList = [f"{x:02d}" for x in range(1, len(nodeNumbers))]
for node in nodeNumList:
constraint = solver.Constraint(1, 1)
for var in variables:
if str(var)[4:6] == node:
constraint.SetCoefficient(var, 1)
# 流量限制式 3 - 流入起點的總和為 0
constraint = solver.Constraint(0, 0)
for var in variables:
if str(var)[4:6] == "00":
constraint.SetCoefficient(var, 1)
# 流量限制式 4 - 流出終點的總和為 0
constraint = solver.Constraint(0, 0)
for var in variables:
if str(var)[1:3] == f"{len(nodeNumbers)-1:02d}":
constraint.SetCoefficient(var, 1)
# 子迴圈限制式: 使用 MTZ 限制式
uVars = [solver.IntVar(0, solver.infinity(), f"u{i:02d}") for i in range(len(nodeNumbers))]
for i, ui in enumerate(uVars):
for j, uj in enumerate(uVars):
for xij in variables:
if i != j and str(ui)[1:3] == str(xij)[1:3] and str(uj)[1:3] == str(xij)[4:6]:
constraint = solver.Constraint(-solver.infinity(), len(nodeNumbers)-1)
constraint.SetCoefficient(ui, 1)
constraint.SetCoefficient(uj, -1)
constraint.SetCoefficient(xij, len(nodeNumbers))
# # 執行以下程式碼會將模式以數學規劃形式印出
# print(solver.ExportModelAsLpFormat(False).replace('\\', '').replace(',_', ','), sep='\n')
# 模式求解
solver.Solve()
odPairs = []
for xij in variables:
if xij.solution_value() > 0:
odPairs.append((int(str(xij)[1:3]), int(str(xij)[4:6])))
# 路徑轉換
route = [odPairs[0][0], odPairs[0][1]]
odPairs.remove(odPairs[0])
while True:
if len(odPairs) < 1:
break
else:
for pair in odPairs:
if pair[0] in route:
route.append(pair[1])
odPairs.remove(pair)
orderList = [nodeNumbers[idx] for idx in route]
return orderList
```
## 方法二:使用 CP-SAT 求解
OR-Tools 提供另一種求解整數規劃的求解器,即 CP-SAT。據官方的說法,若所有的變數皆為整數,使用 CP-SAT 求解會更有效率。
### 求解器和決策變數
首先宣告模型 `model`,並利用 `model.NewIntVar()` 宣告決策變數,函式 args 的填入規則與 MPSolver 求解器中的 `solver.IntVar()` 相同。
```python=
from ortools.sat.python import cp_model
# 宣告模型
model = cp_model.CpModel()
# 建立二元決策變數,xij = {0,1}
variables = [model.NewIntVar(0, 1, f"x{i:02d}_{j:02d}") for i in range(len(nodeNumbers)) for j in range(len(nodeNumbers))]
```
:::info
:bulb: 為了方便 debug,這裡將變數 $x_{01}$ 命名為 `x_00_01`,變數 $x_{46}$ 命名為 `x_04_06`。
:::
接著建立成本矩陣 $c_{ij}$,作法跟上面相同:
```python=
c = timeMatx
# Add penalty term at cii
for i in range(c.shape[0]):
c[i][i] = 1e7
```
### 目標式
目標式為最小化總成本,這裡的總成本就是總旅行時間,即:
$$
\min \space\space \sum_{i}\sum_{j}c_{ij}x_{ij}
$$
這裡會先把所有決策變數的係數先設定好,存到空列表 `objElements`,接著將 `objElements` 丟入 `cp_model.LinearExpr.Sum()` 加總後,再一起包進 `model.Minimize()`,宣告此模式為最小化問題。
:::info
:bulb: 目標式係數一定要 `int`,不可以是 `float`, `double`, ...。
:bulb: `var` 的資料型態是 ortools 的 Variable, 訪問它的話要先轉換成 str。
:::
```python=
objElements = []
for var in variables:
coeff = int(round(c[int(str(var)[1:3])][int(str(var)[4:6])], 0))
objElements.append(var * coeff)
model.Minimize(cp_model.LinearExpr.Sum(objElements))
```
### 流量限制式 1 - 除了終點,其餘節點流出的總和為 1
假設共有 n 個點 (包括起點和終點),起點編號為 $1$,終點的編號為 $n$,則將此流量限制式表示成數學式如下:
$$
\begin{align}
\sum_{i}x_{ij} &= 1 \\
x_{ij} &\in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
nodeNumList = [f"{x:02d}" for x in range(len(nodeNumbers)-1)]
for node in nodeNumList:
constrntElmts = []
for var in variables:
if str(var)[1:3] == node:
constrntElmts.append(var * int(1))
model.Add(cp_model.LinearExpr.Sum(constrntElmts) == 1)
```
### 流量限制式 2 - 除了起點, 其餘節點流入的總和為 1
假設共有 n 個點 (包括起點和終點),起點編號為 $1$,終點的編號為 $n$,則將此流量限制式表示成數學式如下:
$$
\begin{align}
\sum_{i}x_{ij} &= 1 \\
x_{ij} &\in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
nodeNumList = [f"{x:02d}" for x in range(1, len(nodeNumbers))]
for node in nodeNumList:
constrntElmts = []
for var in variables:
if str(var)[4:6] == node:
constrntElmts.append(var * int(1))
model.Add(cp_model.LinearExpr.Sum(constrntElmts) == 1)
```
### 流量限制式 3 - 流入起點的總和為 0
假設共有 n 個點 (包括起點和終點),起點編號為 $1$,終點的編號為 $n$,則將此流量限制式表示成數學式如下:
$$
\begin{align}
\sum_{i}x_{i1} &= 0 \\
x_{ij} &\in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
constrntElmts = []
for var in variables:
if str(var)[4:6] == "00":
constrntElmts.append(var * int(1))
model.Add(cp_model.LinearExpr.Sum(constrntElmts) == 0)
```
### 流量限制式 4 - 流出終點的總和為 0
假設共有 n 個點 (包括起點和終點),起點編號為 $1$,終點的編號為 $n$,則將此流量限制式表示成數學式如下:
$$
\begin{align}
\sum_{j}x_{nj} &= 0 \\
x_{ij} &\in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
constrntElmts = []
for var in variables:
if str(var)[1:3] == f"{len(nodeNumbers)-1:02d}":
constrntElmts.append(var * int(1))
model.Add(cp_model.LinearExpr.Sum(constrntElmts) == 0)
```
### 子迴圈限制式
這裡同樣使用 MTZ 限制式消除子迴圈:
$$
\begin{align}
&u_{i} - u_{j} + nx_{ij} \leq n - 1 \\
&u_{i}, u_{j} \geq 0, \space\space\forall i,j = 1,2,\cdots n, \space\space\forall i \neq j \\
&x_{ij} \in {\{0, 1\}}, \space\space\forall i,j = 1,2,\cdots n
\end{align}
$$
程式碼實作如下:
```python=
uVars = [model.NewIntVar(0, 120, f"u{i:02d}") for i in range(len(nodeNumbers))]
for i, ui in enumerate(uVars):
for j, uj in enumerate(uVars):
for xij in variables:
if i != j and str(ui)[1:3] == str(xij)[1:3] and str(uj)[1:3] == str(xij)[4:6]:
model.Add(1* ui - 1* uj + int(len(nodeNumbers))* xij <= int(len(nodeNumbers)-1))
```
<br>
:::info
:bulb: 執行以下程式碼會將模式以數學規劃形式印出
:::
```python=
print(model.ExportModelAsLpFormat(False).replace('\\', '').replace(',_', ','), sep='\n')
```
### 模式求解
建立求解器物件 `solver`,再呼叫 `solver.Solve()` 函式求解,並選取求解後 >0 的決策變數,表示該兩點間存在一條可行路徑。
程式碼實作如下:
```python=
solver = cp_model.CpSolver()
status = solver.Solve(model)
odPairs = []
for xij in variables:
if solver.Value(xij) > 0:
odPairs.append((int(str(xij)[1:3]), int(str(xij)[4:6])))
```
接著要將 `odPairs` 轉換成路徑。`odPairs` 的長相大概會長這樣:
```python=
# odPairs 長相, 意思是可行路徑分別為 0->4, 1->6, 2->5, ... , 3->7
odPairs = [(0, 4), (1, 6), (2, 5), ... , (3, 7)]
```
我們要做的是將這些起訖對串成一條路徑 `route`,變成 $0-4-1-6-\cdots-3-7$,下面的程式碼即是做這樣的處理,最後將回傳 `orderList`。
```python=
route = [odPairs[0][0], odPairs[0][1]]
odPairs.remove(odPairs[0])
while True:
if len(odPairs) < 1:
break
else:
for pair in odPairs:
if pair[0] in route:
route.append(pair[1])
odPairs.remove(pair)
orderList = [nodeNumbers[idx] for idx in route]
```
### 完整程式碼
```python=
from ortools.sat.python import cp_model
def getHamtPath(timeMatx, nodeNumbers):
""" 使用 CP-SAT 求解器求解最短漢米爾頓路徑 """
# 宣告模型
model = cp_model.CpModel()
# 建立二元決策變數,xij = {0,1}
variables = [model.NewIntVar(0, 1, f"x{i:02d}_{j:02d}") for i in range(len(nodeNumbers)) for j in range(len(nodeNumbers))]
c = timeMatx
for i in range(c.shape[0]):
c[i][i] = 1e7
# 目標式 - 最小化總旅行時間
objElements = []
for var in variables:
coeff = int(round(c[int(str(var)[1:3])][int(str(var)[4:6])], 0))
objElements.append(var * coeff)
model.Minimize(cp_model.LinearExpr.Sum(objElements))
# 流量限制式 1 - 除了終點,其餘節點流出的總和為 1
nodeNumList = [f"{x:02d}" for x in range(len(nodeNumbers)-1)]
for node in nodeNumList:
constrntElmts = []
for var in variables:
if str(var)[1:3] == node:
constrntElmts.append(var * int(1))
model.Add(cp_model.LinearExpr.Sum(constrntElmts) == 1)
# 流量限制式 2 - 除了起點, 其餘節點流入的總和為 1
nodeNumList = [f"{x:02d}" for x in range(1, len(nodeNumbers))]
for node in nodeNumList:
constrntElmts = []
for var in variables:
if str(var)[4:6] == node:
constrntElmts.append(var * int(1))
model.Add(cp_model.LinearExpr.Sum(constrntElmts) == 1)
# 流量限制式 3 - 流入起點的總和為 0
constrntElmts = []
for var in variables:
if str(var)[4:6] == "00":
constrntElmts.append(var * int(1))
model.Add(cp_model.LinearExpr.Sum(constrntElmts) == 0)
# 流量限制式 4 - 流出終點的總和為 0
constrntElmts = []
for var in variables:
if str(var)[1:3] == f"{len(nodeNumbers)-1:02d}":
constrntElmts.append(var * int(1))
model.Add(cp_model.LinearExpr.Sum(constrntElmts) == 0)
# 子迴圈限制式
uVars = [model.NewIntVar(0, 120, f"u{i:02d}") for i in range(len(nodeNumbers))]
for i, ui in enumerate(uVars):
for j, uj in enumerate(uVars):
for xij in variables:
if i != j and str(ui)[1:3] == str(xij)[1:3] and str(uj)[1:3] == str(xij)[4:6]:
model.Add(1* ui - 1* uj + int(len(nodeNumbers))* xij <= int(len(nodeNumbers)-1))
# # 執行以下程式碼會將模式以數學規劃形式印出
# print(model.ExportModelAsLpFormat(False).replace('\\', '').replace(',_', ','), sep='\n')
# 模式求解
solver = cp_model.CpSolver()
status = solver.Solve(model)
odPairs = []
for xij in variables:
if solver.Value(xij) > 0:
odPairs.append((int(str(xij)[1:3]), int(str(xij)[4:6])))
# 路徑轉換
route = [odPairs[0][0], odPairs[0][1]]
odPairs.remove(odPairs[0])
while True:
if len(odPairs) < 1:
break
else:
for pair in odPairs:
if pair[0] in route:
route.append(pair[1])
odPairs.remove(pair)
orderList = [nodeNumbers[idx] for idx in route]
return orderList
```
## 方法三:使用 RoutingModel 求解
OR-Tools 的 RoutingModel 可以更有效率地求解路徑規劃問題,依據筆者的經驗,當模式求解的點位 >10,建議用 RoutingModel 求解,求解效率會快上不少。
下面程式碼是根據[官方的範例程式碼](https://colab.research.google.com/github/google/or-tools/blob/stable/examples/notebook/constraint_solver/vrp_starts_ends.ipynb?hl=zh-tw#scrollTo=code)進行改寫。
### 完整程式碼
```python=
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
def getHamtPath(timeMatx, nodeNumbers):
""" 使用 RoutingModel 求解最短漢米爾頓路徑 """
data = {}
data['distance_matrix'] = timeMatx
data['num_vehicles'] = 1
# 這裡是將矩陣的第0列/第0行設為起點,可以根據需求進行更改
data['starts'] = [0]
# 這裡是將矩陣的最後1列/最後1行設為終點,同樣可以根據需求進行更改
data['ends'] = [int(data['distance_matrix'].shape[0]-1)]
# Create the routing index manager.
manager = pywrapcp.RoutingIndexManager(
len(data["distance_matrix"]), data["num_vehicles"], data["starts"], data["ends"]
)
# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)
# Create and register a transit callback.
def distance_callback(from_index, to_index):
"""Returns the distance between the two nodes."""
# Convert from routing variable Index to distance matrix NodeIndex.
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return data["distance_matrix"][from_node][to_node]
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
# Define cost of each arc.
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# Add Distance constraint.
dimension_name = "Distance"
routing.AddDimension(
transit_callback_index,
0, # no slack
86400, # vehicle maximum travel distance
True, # start cumul to zero
dimension_name,
)
distance_dimension = routing.GetDimensionOrDie(dimension_name)
distance_dimension.SetGlobalSpanCostCoefficient(100)
# Setting first solution heuristic.
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.SAVINGS
)
# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)
index = routing.Start(0)
orderList = [nodeNumbers[index]]
while not routing.IsEnd(index):
index = solution.Value(routing.NextVar(index))
orderList.append(nodeNumbers[index])
return orderList
```
## 最佳化演算結果
無論你是用上面三種方法的任意一種,基本上求出來的解都會相同,以下為求解的範例程式碼:
```python=
path = getHamtPath(timeMatx, nodeNumbers)
print(path)
# Output
# [0, 1, 6, 4, 3, 5, 2, 7]
```
將結果繪製於地圖上,這裡使用 `folium` 套件進行地圖繪製:
```python=
import folium
numCurryShops = len(curryShops)
myMap = folium.Map(location=[nodeList[0][1], nodeList[0][0]], zoom_start=13, tiles='CartoDB positron')
for node in path:
if node > numCurryShops:
folium.Marker(location=[nodeList[node][1], nodeList[node][0]], icon=folium.Icon(color='orange', icon='cloud')).add_to(myMap)
else:
folium.Marker(location=[nodeList[node][1], nodeList[node][0]], icon=folium.Icon(color='blue', icon='cloud')).add_to(myMap)
folium.Marker(location=[nodeList[0][1], nodeList[0][0]], icon=folium.Icon(color='pink', icon='cloud')).add_to(myMap)
folium.PolyLine([(nodeList[node][1], nodeList[node][0]) for node in path], color='cyan', opacity=0.6).add_to(myMap)
folium.TileLayer('cartodbdark_matter').add_to(myMap)
display(myMap)
```

<br>
## 延伸閱讀
- [最短漢米爾頓路徑問題 (I) - 數學模式說明](https://hackmd.io/kdviO126QJq6B4YfxrGzwQ)
- [OSRM Python API 使用說明](https://hackmd.io/1kdmHn2xTGS1jjLzC5kVTA)