現實世界的許多問題,例如資源分配、路線規劃和生產排程,都可以歸結為最佳化問題。解決這類別問題的關鍵在於建立準確的數學模型,並利用有效的演算法找到最佳解。本文以尋找最佳路線和兩棲動物共存問題為例,說明如何使用數學建模和程式設計工具求解最佳化問題。首先,我們需要定義目標函式,它代表我們想要最大化或最小化的指標,例如時間、成本或資源利用率。其次,我們需要確定約束條件,這些條件限制了可行的解決方案範圍,例如資源限制、物理限制或邏輯限制。最後,我們需要定義決策變數,這些變數的值決定了最終的解決方案。透過將這些要素整合到一個數學模型中,我們可以使用最佳化演算法,例如 Google OR-Tools 提供的演算法,來找到最佳解。
最佳化問題的建模與求解
在現實生活中,我們經常會遇到各種需要做出最佳決策的問題,例如尋找最快的路線、最優的資源分配方案等。這些問題都可以歸類別為最佳化問題。最佳化問題的核心是找到在給定約束條件下的最優解。
最佳化問題的建模
最佳化問題的建模是指將現實問題轉化為數學模型,以便使用數學方法進行求解。一個完整的最佳化模型通常包括兩個主要部分:目標函式和約束條件。
- 目標函式:目標函式是指我們想要最佳化的量,例如時間、成本、距離等。最佳化問題可以是最大化或最小化目標函式。
- 約束條件:約束條件是指限制我們決策的因素,例如資源限制、物理限制等。
以「最佳路線」問題為例
假設我們要騎腳踏車送兩歲的兒子去日託中心,並且希望盡快到達。這裡的目標函式是最小化行駛時間,而約束條件包括必須遵循街道或腳踏車道、遵守交通規則等。
程式碼實作
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
def create_data_model():
"""Stores the data for the problem."""
data = {}
data['distance_matrix'] = [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]
]
data['num_vehicles'] = 1
data['depot'] = 0
return data
def print_solution(manager, routing, solution):
"""Prints solution on console."""
print('Objective: {}'.format(solution.ObjectiveValue()))
index = routing.Start(0)
plan_output = 'Route for vehicle 0:\n'
route_distance = 0
while not routing.IsEnd(index):
plan_output += ' {} ->'.format(manager.IndexToNode(index))
previous_index = index
index = solution.Value(routing.NextVar(index))
route_distance += routing.GetArcCostForVehicle(previous_index, index, 0)
plan_output += ' {}\n'.format(manager.IndexToNode(index))
plan_output += 'Route distance: {}\n'.format(route_distance)
print(plan_output)
def main():
"""Entry point of the program."""
# Instantiate the data problem.
data = create_data_model()
# Create the routing index manager.
manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
data['num_vehicles'],
data['depot'])
# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)
def distance_callback(from_index, to_index):
"""Returns the distance between the two nodes."""
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)
# Setting first solution heuristic.
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)
# Print solution on console.
if solution:
print_solution(manager, routing, solution)
if __name__ == '__main__':
main()
內容解密:
create_data_model函式:此函式建立並傳回一個包含問題資料的字典。資料包括距離矩陣、車輛數量和出發點(depot)。print_solution函式:此函式用於列印解決方案的詳細資訊,包括目標值和路線。main函式:程式的入口點。它例項化資料問題,建立路由索引管理器和路由模型,並定義距離回撥函式來計算兩點之間的距離。distance_callback函式:此函式根據距離矩陣傳回兩節點之間的距離。- 搜尋引數設定:設定搜尋引數並選擇第一個解決方案策略為
PATH_CHEAPEST_ARC,即根據最便宜的路徑尋找初始解。 - 求解問題:使用設定的搜尋引數求解路由問題。
- 列印結果:如果找到解決方案,則呼叫
print_solution函式列印結果。
使用 Google OR-Tools 求解最佳化問題
Google OR-Tools 提供了一套強大的工具,用於求解各種最佳化問題,包括路由問題、排程問題等。在上述範例中,我們使用 OR-Tools 的路由模組來求解最佳路線問題。
1.2.2 符號表示說明
在整本文中,我們將描述多個代數模型,並使用兩種方式來表示這些模型。首先,我們會使用常見的數學符號來描述每個模型,接著使用可執行的 Python 程式碼來完整呈現詳細的模型。這兩種表示方式之間的對應關係應該很容易理解。表 1-1 列出了數學模式與 Python 模式之間的對應關係範例。
數學與程式碼表示的對等性
| 物件 | 數學模式 | Python 模式 |
|---|---|---|
| 標量變數 | X | X |
| 向量 | $v_i$ | v[i] |
| 矩陣 | $M_{ij}$ | M[i][j] |
| 不等式 | x + y ≤ 10 | x + y <= 10 |
| 累加 | $\sum_{i=0}^{9} x_i$ | sum(x[i] for i in range(10)) |
| 集合定義 | {$i^2$ | i ∈ [0, 1, …, 9]} | [i**2 for i in range(10)] |
1.3 初步實踐:兩棲動物的共存問題
最簡單的問題與高中時遇到的「文字題」相似,它們本質上是代數問題,可以使用基礎線性代數工具來建模和解決。讓我們考慮一個這樣的問題來說明建模方法,並定義一些基本概念。
問題描述
某動物園生物學家計劃將三種兩棲動物(蟾蜍、蠑螈和蚓螈)放入一個水族箱中,這些動物會以三種不同的小型獵物為食:蚯蚓、蟋蟀和蒼蠅。每天,水族箱中會放入 1,500 條蚯蚓、3,000 隻蟋蟀和 5,500 隻蒼蠅。每種兩棲動物每天會消耗一定數量的獵物。表 1-2 總結了相關資料。
表 1-2:每種兩棲動物消耗的獵物數量
| 食物 | 蟾蜍 | 蠑螈 | 蚓螈 | 可用數量 |
|---|---|---|---|---|
| 蚯蚓 | 2 | 1 | 1 | 1500 |
| 蟋蟀 | 1 | 3 | 2 | 3000 |
| 蒼蠅 | 1 | 2 | 3 | 5500 |
建模步驟
所有最佳化和可行性問題都遵循三步驟建模方法:
- 明確問題:提出一個精確的問題陳述,涉及計算或評估一個或多個物件。在本例中,問題是「每種兩棲動物可以共存多少隻?」
- 為了回答這個問題,我們需要定義決策變數:$x_0$(蟾蜍數量)、$x_1$(蠑螈數量)和 $x_2$(蚓螈數量)。
程式碼實作:定義決策變數
# 定義決策變數
x0 = 0 # 蟾蜍數量
x1 = 0 # 蠑螈數量
x2 = 0 # 蚓螈數量
內容解密:
- 這段程式碼定義了三個變數,分別代表三種兩棲動物的數量。
- 初始化為0,表示初始狀態下水族箱中沒有任何兩棲動物。
識別需求並轉換為約束條件:
- 所有兩棲動物總共消耗的蚯蚓不超過 1,500。
- 所有兩棲動物總共消耗的蟋蟀不超過 3,000。
- 所有兩棲動物總共消耗的蒼蠅不超過 5,500。
將這些需求轉化為代數約束條件:
- $2x_0 + x_1 + x_2 \leq 1500$(蚯蚓約束)
- $x_0 + 3x_1 + 2x_2 \leq 3000$(蟋蟀約束)
- $x_0 + 2x_1 + 3x_2 \leq 5500$(蒼蠅約束)
程式碼實作:定義約束條件
# 定義約束條件
def constraint_worms(x0, x1, x2):
return 2*x0 + x1 + x2 <= 1500
def constraint_crickets(x0, x1, x2):
return x0 + 3*x1 + 2*x2 <= 3000
def constraint_flies(x0, x1, x2):
return x0 + 2*x1 + 3*x2 <= 5500
內容解密:
- 這裡定義了三個函式,每個函式代表一個約束條件。
- 每個函式檢查相應的資源(蚯蚓、蟋蟀、蒼蠅)是否滿足約束條件。
- 例如,
constraint_worms函式檢查 $2x_0 + x_1 + x_2 \leq 1500$ 是否成立。
- 確定最佳化目標:在本問題中,我們希望最大化共存的兩棲動物總數,即最大化 $x_0 + x_1 + x_2$。
程式碼實作:目標函式
# 定義目標函式
def objective(x0, x1, x2):
return x0 + x1 + x2
內容解密:
- 此函式計算並傳回三種兩棲動物的總數。
- 目的是在滿足所有約束條件的前提下,最大化這個總數。
使用通用程式語言與最佳化函式庫建立數學模型
在建立數學模型後,下一步是將其轉化為可被各種求解器理解的形式。多年來,最佳化領域已經發展出多種專門的建模語言和求解器。
專門的建模語言與求解器
以下是一些常見的建模語言和求解器:
- 建模語言:
- AMPL (www.ampl.com)
- GAMS (www.gams.com)
- GMPL (http://en.wikibooks.org/wiki/GLPK/GMPL (MathProg))
- Minizinc (www.minizinc.org/)
- OPL (www-01.ibm.com/software/info/ilog/)
- ZIMPL (http://zimpl.zib.de/)
- 求解器:
- CBC (www.coin-or.org/)
- CLP (www.coin-or.org/Clp/)
- CPLEX (www-01.ibm.com/software/info/ilog/)
- ECLiPSe (http://eclipseclp.org/)
- Gecode (www.gecode.org/)
- GLOP (https://developers.google.com/optimization/lp/glop)
- GLPK (www.gnu.org/software/glpk/)
- Gurobi (www.gurobi.com/)
- SCIP (http://scip.zib.de/)
為何選擇通用程式語言?
雖然專門的建模語言具有簡潔的表達方式,但它們往往在某個時候會遇到限制,需要額外的程式碼來連線模型和應用程式的其他部分。相比之下,使用通用程式語言(如 Python)結合最佳化函式庫,可以更靈活地完成建模和應用程式整合的工作。
本文將使用 Google 的 Operations Research Tools(OR-Tools)函式庫,它提供了優秀的介面來存取多種線性和整數求解器,並且具有特殊的網路流問題和限制式程式設計功能。
範例:兩棲動物共存模型
以下是一個使用 OR-Tools 建立的兩棲動物共存模型範例:
from ortools.linear_solver import pywraplp
def solve_coexistence():
t = 'Amphibian coexistence'
s = pywraplp.Solver(t, pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
x = [s.NumVar(0, 1000, 'x[%i]' % i) for i in range(3)]
pop = s.NumVar(0, 3000, 'pop')
s.Add(2*x[0] + x[1] + x[2] <= 1500)
s.Add(x[0] + 3*x[1] + 2*x[2] <= 3000)
s.Add(x[0] + 2*x[1] + 3*x[2] <= 4000)
s.Add(pop == x[0] + x[1] + x[2])
s.Maximize(pop)
s.Solve()
return pop.SolutionValue(), [e.SolutionValue() for e in x]
程式碼解析:
- 載入 OR-Tools 的 Python 包裝器,用於線性規劃。
- 定義
solve_coexistence函式,建立一個線性規劃求解器。 - 建立三個決策變數
x,代表三種兩棲動物的數量,範圍在 0 到 1000 之間。 - 建立一個變數
pop,代表總人口數,範圍在 0 到 3000 之間。 - 新增三個限制條件,分別對應數學表示式(1.1)-(1.3)。
- 將總人口數
pop設定為三種兩棲動物數量的總和。 - 將目標函式設定為最大化總人口數
pop。 - 求解模型,並傳回總人口數和三種兩棲動物的數量。
使用通用程式語言和最佳化函式庫,可以更靈活地建立和求解數學模型,並且可以輕鬆地將模型整合到更大的應用程式中。OR-Tools 提供了優秀的介面和功能,可以簡化建模和求解的過程。
線性連續模型
在最佳化領域的黎明時期(二十世紀五十年代),線性最佳化模型和單純形法是當時的技術主流。單純形法是當時唯一能夠有效解決這類別模型的演算法。筆者在開始研究這個主題時,曾多次聽到一種說法:全球超過70%的CPU運算週期都被用來執行各種單純形法程式。雖然這可能有所誇張,但它反映了線性模型的強大威力。現實世界並非總是線性的,但線性近似往往已經足夠。
線性連續模型的定義
嚴格來說,本章討論的是線性連續模型(儘管通常將這些模型簡稱為LP,即線性規劃,預設其具有連續性)。線性連續模型是最簡單的模型之一,無論是在表述還是求解上都相對容易。自從George Dantzig發明瞭單純形法來求解這類別模型以來,它們就成了最佳化領域的主力工具。線性連續模型的特點包括三個要素:
- 所有變數都是連續的。
- 所有約束條件都是線性的。
- 目標函式是線性的。
詳細特性
具體而言,決策變數(例如$x_0, … , x_n$)可以取整數或分數值。當解決方案涉及測量或分配連續資源時,這種特性非常合適。
# 範例程式碼:定義一個簡單的線性連續模型
from ortools.linearsolver import pywraplp
def solve_linear_continuous_model():
# 建立一個線性規劃求解器
solver = pywraplp.Solver.CreateSolver('GLOP')
# 宣告變數
x = [solver.NumVar(0, solver.infinity(), f'x{i}') for i in range(3)]
# 新增約束條件
constraint1 = solver.Constraint(0, 1500)
constraint1.SetCoefficient(x[0], 2)
constraint1.SetCoefficient(x[1], 1)
constraint1.SetCoefficient(x[2], 1)
# 設定目標函式
objective = solver.Objective()
for var in x:
objective.SetCoefficient(var, 1)
objective.SetMaximization()
# 求解模型
status = solver.Solve()
# 傳回結果
if status == pywraplp.Solver.OPTIMAL:
return solver.Objective().Value(), [var.solution_value() for var in x]
else:
return None, None
#### 內容解密:
此範例程式碼展示瞭如何使用Google的OR-Tools函式庫建立和求解一個簡單的線性連續模型。首先,我們建立了一個線性規劃求解器例項,並聲明瞭一組連續變數。接著,我們增加了一個線性約束條件,並定義了一個簡單的線性目標函式,旨在最大化變數的總和。最後,我們呼叫求解器來計算最優解,並傳回目標函式的最優值和決策變數的最優值。
#### 進一步解析:
1. **建立求解器**:我們使用`pywraplp.Solver.CreateSolver('GLOP')`建立了一個根據GLOP後端的線性規劃求解器。GLOP是Google開發的一個高效的線性規劃求解器。
2. **變數宣告**:透過`solver.NumVar`方法,我們聲明瞭一組連續變數$x_0, x_1, x_2$,每個變數的取值範圍從0到無窮大。
3. **約束條件**:我們建立了一個約束條件$2x_0 + x_1 + x_2 \leq 1500$,並透過`SetCoefficient`方法設定了變數的係數。
4. **目標函式**:目標函式被設定為最大化$x_0 + x_1 + x_2$,體現了線性目標函式的特性。
5. **求解與結果**:呼叫`solver.Solve()`進行求解後,我們檢查瞭解的狀態。如果狀態為`OPTIMAL`,則傳回最優目標函式值和最優變數值。
### 線性連續模型的優勢
線性連續模型由於其簡單性和成熟的求解演算法(如單純形法和內點法),在眾多領域得到了廣泛應用。它們特別適合於資源分配、生產排程、投資組合最佳化等問題。