選址問題是要選擇設施位置使目標達到最優,是數模競賽中的常見題型。
小白不一定要掌握所有的選址問題,但要能判斷是哪一類問題,用哪個模型。
進一步學習 PuLP工具包中處理複雜問題的字典格式快捷建模方法。
歡迎關注『Python小白的數學建模課 @ Youcans』系列,每週持續更新
1. 選址問題
選址問題是指在某個區域內選擇設施的位置使所需的目標達到最優。選址問題也是一種互斥的計劃問題。
例如投資場所的選址:企業要在 m 個候選位置選擇若干個建廠,已知建廠費用、運輸費及 n 個地區的產品需求量,應如何進行選址。
選址問題是運籌學中經典的問題之一,選址問題在生產生活、物流、甚至軍事中都有著非常廣泛的應用,如工廠、倉庫、急救中心、消防站、垃圾處理中心、物流中心、導彈倉庫的選址等。更重要的,選址問題也是數模競賽的熱點問題。
選址是重要的長期決策,選址的好壞直接影響到服務方式、服務質量、服務效率、服務成本等,從而影響到利潤和市場競爭力,選址問題的研究有著重大的經濟、社會和軍事意義。
選址問題有四個基本要素:設施、區域、距離和優化目標。
1.1 設施
選址問題中所說的設施,在具體題目中可以是工廠、倉庫、服務站等形式。
1.2 區域
選址問題中所說的區域,在具體題目中可以是工廠、車間的內部佈局,也可以是給定的某個地區、甚至空間範圍。
按照規劃區域的特徵,可以分為連續選址問題和離散選址問題。連續選址問題,設施可以佈局在區域內的任意位置,就要求出最優選址的座標;離散選址問題,只能從若干候選位置中進行選擇,運籌學中的選址問題通常是這類離散選址問題。
1.3 距離
選址問題中所說的距離,是指設施到服務物件之間的距離,在具體題目中也可以是某個選址位置的服務時間、成本、覆蓋範圍。如果用圖論方法求解,通常就是連線頂點的邊的權值。
當問題所關注的是設施到服務物件之間的距離時,如果問題給出的不是頂點之間的距離,而是設施的位置座標,要注意不是隻有歐式距離,對於不同問題也可能是球面距離、曼哈頓距離、切比雪夫距離。
1.4 優化目標
選址問題要求選擇最好的選址位置,但選址位置只是決策變數,選擇的最終目的通常是實現加權距離最短、費用最小、利潤最大、時間最短,這才是優化問題的目標函式。
按照目標函式的特點,可以分為:中位問題,要求總成本最小;中心問題,服務於每個客戶的最大成本最小;反中心問題:服務於每個客戶的最小成本最大。
2. 常見選址問題及建模
2.1 P-中位問題(P-median problem)
P-中位問題,假設有 N 個候選服務站和 M 個需求點,要從 N 個候選服務站中選擇 P 個,使所有需求點到最近的服務站的加權距離 \(d_{ij}\) 的總和最小。需求點 i 的權值,通常是指該需求點的需求量。
這是一個 MinSum 問題,定義決策變數 \(x_j\) 為選中的服務站,\(y_{ij}\) 將各需求點匹配到最近的服務站:
可以建立數學模型如下:
其中:j 為服務站,i 為需求點,\(w_i\) 為需求點 i 的需求量, \(d_{ij}\) 為需求點 i 到服務站 j 的距離。
2.2 P-中心問題
P-中心問題,假設有 N 個候選服務站和 M 個需求點,要從 N個候選服務站中選擇 P個,使任一需求點到最近的服務站的最大距離最小。
這是一個 MinMax 問題,需要最小化任何需求點與其鄰近設施點的最大距離。P-中位問題追求總和最小,可以理解為發展經濟總量優先;P-中心問題關注最差個體的最好結果,可以理解為優先進行扶貧。
定義決策變數 \(x_j\) 為選中的服務站,\(y_{ij}\) 將各需求點匹配到最近的服務站:
可以建立數學模型如下:
其中:j 為服務站,i 為需求點, \(d_{ij}\) 為需求點 i 到服務站 j 的距離。如果只求需求點到最近的服務站的最大距離,則 \(w_i = 1\) ;如果要求任一需求點到最近的服務站的最大運費,則 \(w_i\) 為需求點 i 的需求量,即加權最大距離。
2.3 集合覆蓋問題
覆蓋模型適用於一些特殊場景,例如消防中心、救護車、巡邏車等應急設施的區位選址問題。覆蓋問題分為集合覆蓋問題(Set covering problem)和最大覆蓋問題(Maximal covering problem)。
集合覆蓋問題研究滿足覆蓋所有需求點顧客的前提下,服務站的最少個數或建設費用最小的問題。假設有 N 個候選服務站和 M 個需求點,已知每個服務站的服務範圍(或服務容量),要從 N個候選服務站中選擇若干個,使所有需求點得到服務(到所屬服務站的距離或時間小於給定的臨界值),服務站的個數最少或成本最小。
定義引數 \(a_{ij}\) 為每個服務站的覆蓋範圍:
定義決策變數 \(x_j\) 為選中的服務站:
可以建立數學模型如下:
其中:j 為服務站,i 為需求點,\(c_j\) 為服務站 j 的建設費用(最少個數問題中不需要考慮),\(N_i=\{j:a_{ij}=1\}\) 是覆蓋需求點 i 的候選服務站的集合。
2.4 最大覆蓋問題
最大覆蓋問題研究在已知服務站的數目和服務半徑的條件下,如何設立 P個服務站使得可接受的服務需求最大的問題。
定義決策變數 \(x_j\) 為選中的服務站:
可以建立數學模型如下:
其中:j 為服務站,i 為需求點,\(w_i\) 為需求點 i 的需求量。
2.5 其它選址問題
其它選址問題,在數學建模中應用相對較少,限於篇幅不能逐一介紹其數學模型。在此將各模型的特點簡要介紹,以便判斷問題的型別。
帶固定費用和容量限制的選址問題
服務站建站的固定費用和服務站的容量(能力)限制這兩個因素具有很強的實際意義,經常作為基本選址問題的深化研究課題。
無容量限制的固定費用下的選址問題,就是去掉服務站個數的約束,並將固定建站費用加到 P-中位問題的目標函式上。
選址分配問題
選址分配問題類似於 P-中位問題,有 m 個服務站需要選址,n 個已知位置的顧客分配給不同的設施,已知每個服務站的能力和每個顧客的需求,要求服務站的選址和顧客對服務站的分配,使顧客與所分配服務站的距離總和最小。
隨機選址問題
服務站的執行時間、建設成本、需求點位置、需求數量等全部或部分引數是不確定的,但服從某種隨機分佈。
動態選址問題
研究未來若干時間段內服務站的最優選址問題,在不同時間段內動態選址模型的引數是變化的,但在某一時間段內模型引數是確定的。
競爭選址問題
研究考慮市場上存在兩個以上同類產品或服務的提供者,或服務站提供多個產品或服務。
2.6 選址問題的求解演算法與程式設計實現
設施選址問題通常是是 NP 問題,不存在多項式時間演算法。常用的近似解法有:
線性規劃舍入演算法,相當於整數規劃問題的求解演算法。首先給出原問題的整數規劃模型,然後求解相應的線性規劃鬆弛問題得到分數最優解,根據可行要求對分數最優解進行改造,構造原問題的整數可行解。
原始對偶演算法,首先找到對偶問題的一個可行解,再根據該對偶可行解構造原始問題的整數可行解,不斷調整對偶問題的可行解,直到找到最優解為止。
區域性搜尋演算法:給定初始可行解,定義適當的鄰域,通過引入恰當的調整策略,在鄰域中得到改進的可行解,依次迭代,直到調整策略不能改進為止
啟發式演算法或隨機優化演算法。
本節作為線性規劃問題系列的一篇,仍然選擇 PuLP工具包求解選址問題。很多選址問題適合用圖論方法描述和求解,這將在後續課程中進行介紹。
3. 案例 1:PuLP求解指派問題
說明:本案例是指派問題,不是選址問題。因指派問題未單獨成文,因此將該案例放在本文中。
另外,本案例給出了 PuLP 工具包使用字典方式快捷程式設計的使用方法,這在選址問題中是非常方便的。
3.1 游泳接力賽的指派問題
游泳隊中 A、B、C、D 四名運動員組成 4x100米混合泳接力隊,運動員各種泳姿的成績如下表所示:
隊員\專案 | 自由泳 | 蛙泳 | 蝶泳 | 仰泳 |
---|---|---|---|---|
A | 56 | 74 | 61 | 63 |
B | 63 | 69 | 65 | 71 |
C | 57 | 77 | 63 | 67 |
D | 55 | 76 | 62 | 62 |
如何安排 A、B、C、D 四名運動員的泳姿,才最有可能取得好成績?
3.2 指派問題建模分析
引入 0-1 變數 \(x_{ij}\):
指派問題的數學模型就可以描述為:
其中:
3.3 指派問題模型求解的程式設計
模型求解,用標準模型的優化演算法對模型求解,得到優化結果。模型求解的程式設計步驟如下:
(0)匯入 PuLP庫函式
import pulp
(1)定義一個規劃問題
AssignLP = pulp.LpProblem("Assignment_problem_for_swimming_relay_race", sense=pulp.LpMinimize)
pulp.LpProblem 用來定義問題的建構函式。引數 sense 指定問題求目標函式的最小值/最大值 。
(2)定義決策變數
rows = cols = range(0, 4)
x = pulp.LpVariable.dicts("x", (rows, cols), cat="Binary")
pulp.LpVariable 用來定義決策變數的函式。引數 cat 設定變數型別,' Binary ' 表示 0/1 變數。
注意,指派問題、選址問題中都涉及 N*M 維矩陣變數,變數個數很多,如果逐一定義非常冗長,而且容易出錯、不便修改。本例使用 pulp.LpVariable.dicts 提供的字典格式定義了 4*4 個變數 \(x_{ij}\),使程式大為簡化。
(3)新增目標函式
scoreM = [[56,74,61,63],[63,69,65,71],[57,77,63,67],[55,76,62,62]]
AssignLP += pulp.lpSum([[x[row][col]*scoreM[row][col] for row in rows] for col in cols])
本例程在語句內使用兩重 for 迴圈遍歷列表實現所有變數的線性組合 ,使程式大為簡化。
(4)新增約束條件
for row in rows:
AssignLP += pulp.lpSum([x[row][col] for col in cols]) == 1 # sum(x(i,j),j=1,4)=1, i=1,4
for col in cols:
AssignLP += pulp.lpSum([x[row][col] for row in rows]) == 1 # sum(x(i,j),i=1,4)=1, j=1,4
快捷方法對於約束條件的定義與對目標函式的定義相似,使用字典定義引數,使用迴圈定義約束條件,使程式簡單、結構清楚。
(5)求解和結果輸出
AssignLP.solve() # youcans
print(AssignLP.name)
member = ["隊員A","隊員B","隊員C","隊員D"]
style = ["自由泳","蛙泳","蝶泳","仰泳"]
if pulp.LpStatus[AssignLP.status] == "Optimal": # 獲得最優解
xValue = [v.varValue for v in AssignLP.variables()]
# [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]
xOpt = np.array(xValue).reshape((4, 4)) # 將 xValue 格式轉換為 4x4 矩陣
print("最佳分配:" )
for row in rows:
print("{}\t{} 參加專案:{}".format(xOpt[row],member[row],style[np.argmax(xOpt[row])]))
print("預測最好成績為:{}".format(pulp.value(AssignLP.objective)))
xValue 獲得的是列表變數,通過 numpy 的 reshape() 函式轉換為 4*4 矩陣,便於格式化輸出。
3.4 指派問題 Python 例程
# mathmodel08_v1.py
# Demo08 of mathematical modeling algorithm
# Solving assignment problem with PuLP.
# Copyright 2021 Youcans, XUPT
# Crated:2021-06-02
# Python小白的數學建模課 @ Youcans
import pulp # 匯入 pulp 庫
import numpy as np
# 主程式
def main():
# 問題建模:
"""
決策變數:
x(i,j) = 0, 第 i 個人不遊第 j 種姿勢
x(i,j) = 1, 第 i 個人遊第 j 種姿勢
i=1,4, j=1,4
目標函式:
min time = sum(sum(c(i,j)*x(i,j))), i=1,4, j=1,4
約束條件:
sum(x(i,j),j=1,4)=1, i=1,4
sum(x(i,j),i=1,4)=1, j=1,4
變數取值範圍:
x(i,j) = 0,1
"""
# 游泳比賽的指派問題 (assignment problem)
# 1.建立優化問題 AssignLP: 求最小值(LpMinimize)
AssignLP = pulp.LpProblem("Assignment_problem_for_swimming_relay_race", sense=pulp.LpMinimize) # 定義問題,求最小值
# 2. 建立變數
rows = cols = range(0, 4)
x = pulp.LpVariable.dicts("x", (rows, cols), cat="Binary")
# 3. 設定目標函式
scoreM = [[56,74,61,63],[63,69,65,71],[57,77,63,67],[55,76,62,62]]
AssignLP += pulp.lpSum([[x[row][col]*scoreM[row][col] for row in rows] for col in cols])
# 4. 施加約束
for row in rows:
AssignLP += pulp.lpSum([x[row][col] for col in cols]) == 1 # sum(x(i,j),j=1,4)=1, i=1,4
for col in cols:
AssignLP += pulp.lpSum([x[row][col] for row in rows]) == 1 # sum(x(i,j),i=1,4)=1, j=1,4
# 5. 求解
AssignLP.solve() # youcans
# 6. 列印結果
print(AssignLP.name)
member = ["隊員A","隊員B","隊員C","隊員D"]
style = ["自由泳","蛙泳","蝶泳","仰泳"]
if pulp.LpStatus[AssignLP.status] == "Optimal": # 獲得最優解
xValue = [v.varValue for v in AssignLP.variables()]
# [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]
xOpt = np.array(xValue).reshape((4, 4)) # 將 xValue 格式轉換為 4x4 矩陣
print("最佳分配:" )
for row in rows:
print("{}\t{} 參加專案:{}".format(xOpt[row],member[row],style[np.argmax(xOpt[row])]))
print("預測最好成績為:{}".format(pulp.value(AssignLP.objective)))
return
if __name__ == '__main__': # Copyright 2021 YouCans, XUPT
main() # Python小白的數學建模課 @ Youcans
3.5 Python 例程執行結果
Welcome to the CBC MILP Solver
Version: 2.9.0
Build Date: Feb 12 2015
Result - Optimal solution found
Assignment_problem_for_swimming_relay_race
最佳分配:
[0. 0. 1. 0.] 隊員A 參加專案:蝶泳
[0. 1. 0. 0.] 隊員B 參加專案:蛙泳
[1. 0. 0. 0.] 隊員C 參加專案:自由泳
[0. 0. 0. 1.] 隊員D 參加專案:仰泳
預測最好成績為:249.0
4. 案例 2:PuLP求解選址問題
4.1 消防站的選址問題
例題 2:某城市有 8 個區,每個區最多建一個消防站,擬建設消防站到各區的最長時間如下表所示。現要求任何區域發生火警時,消防車能在 10分鐘內趕到。在此條件下儘量減少消防站數量,應該在哪幾個區建設消防站?
區域 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
1 | 7 | 12 | 18 | 20 | 24 | 26 | 25 | 28 |
2 | 14 | 5 | 8 | 15 | 16 | 18 | 18 | 18 |
3 | 19 | 9 | 4 | 14 | 10 | 22 | 16 | 13 |
4 | 14 | 15 | 15 | 10 | 18 | 15 | 14 | 18 |
5 | 20 | 18 | 12 | 20 | 9 | 25 | 14 | 12 |
6 | 18 | 21 | 20 | 16 | 20 | 6 | 10 | 15 |
7 | 22 | 18 | 20 | 15 | 16 | 15 | 5 | 9 |
8 | 30 | 22 | 15 | 20 | 14 | 18 | 8 | 6 |
4.2 選址問題建模分析
首先判斷這是一個集合覆蓋問題,要求從 8 個候選消防站中選擇若干個,在所有需求點得到服務的時間都小於臨界值 10分鐘的條件下,選擇消防站的數量最少。本問題不考慮各候選站點建設費用的差異,即不帶權重。
定義引數 \(R_{ij}\) 為每個消防站的覆蓋範圍:
由擬建消防站到各區的最長時間表可以得到引數 \(R_{ij}\) 如下表:
區域 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
3 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 |
4 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
5 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
6 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
7 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
8 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
定義決策變數 \(x_j\) 為選中的服務站:
可以建立數學模型如下:
選址問題的模型求解,用標準模型的優化演算法對模型求解,得到優化結果。
模型求解的程式設計步驟與指派問題是一致的,且在例程中給出了詳細的註釋,就不再進行逐項解釋了。
需要注意的是,選址問題的決策變數、引數、約束條件的數量較大(N*M),如果對變數、約束條件逐個進行定義,程式設計過程將是非常冗長和痛苦的,因此需要使用列表、字典等快捷方式進行定義。對於更大規模的問題,模型中的資料要通過讀取資料檔案獲得,就更需要採用這種方式來程式設計。
4.3 選址問題 Python 例程
# mathmodel09_v1.py
# Demo08 of mathematical modeling algorithm
# Solving set covering problem with PuLP.
# Copyright 2021 Youcans, XUPT
# Crated:2021-06-06
# Python小白的數學建模課 @ Youcans
import pulp # 匯入 pulp 庫
# 主程式
def main():
# 問題建模:
"""
決策變數:
x(j) = 0, 不選擇第 j 個消防站
x(j) = 1, 選擇第 j 個消防站, j=1,8
目標函式:
min fx = sum(x(j)), j=1,8
約束條件:
sum(x(j)*R(i,j),j=1,8) >=1, i=1,8
變數取值範圍:
x(j) = 0,1
"""
# 消防站的選址問題 (set covering problem, site selection of fire station)
# 1.建立優化問題 SetCoverLP: 求最小值(LpMinimize)
SetCoverLP = pulp.LpProblem("SetCover_problem_for_fire_station", sense=pulp.LpMinimize) # 定義問題,求最小值
# 2. 建立變數
zones = list(range(8)) # 定義各區域 youcans
x = pulp.LpVariable.dicts("zone", zones, cat="Binary") # 定義 0/1 變數,是否在該區域設消防站
# 3. 設定目標函式
SetCoverLP += pulp.lpSum([x[j] for j in range(8)]) # 設定消防站的個數
# 4. 施加約束
reachable = [[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 0, 1, 1]] # 引數矩陣,第 i 消防站能否在 10分鐘內到達第 j 區域
for i in range(8):
SetCoverLP += pulp.lpSum([x[j]*reachable[j][i] for j in range(8)]) >= 1
# 5. 求解
SetCoverLP.solve()
# 6. 列印結果
print(SetCoverLP.name)
temple = "區域 %(zone)d 的決策是:%(status)s" # 格式化輸出
if pulp.LpStatus[SetCoverLP.status] == "Optimal": # 獲得最優解
for i in range(8):
output = {'zone': i+1, # 與問題中區域 1~8 一致
'status': '建站' if x[i].varValue else '--'}
print(temple % output)
print("需要建立 {} 個消防站。".format(pulp.value(SetCoverLP.objective)))
return
if __name__ == '__main__': # Copyright 2021 YouCans, XUPT
main() # Python小白的數學建模課 @ Youcans
4.4 Python 例程執行結果
Welcome to the CBC MILP Solver
Version: 2.9.0
Build Date: Feb 12 2015
Result - Optimal solution found
SetCover_problem_for_fire_station
區域 1 的決策是:建站
區域 2 的決策是:--
區域 3 的決策是:建站
區域 4 的決策是:建站
區域 5 的決策是:--
區域 6 的決策是:建站
區域 7 的決策是:建站
區域 8 的決策是:--
需要建立 5.0 個消防站
5. 小結
- 關於規劃問題,我們從線性規劃、整數規劃、0-1規劃到一些特殊型別問題,用 5節課進行了介紹,到這裡就暫告一段落了。後面根據需要,可能還會講非線性規劃,實際上主要是非線性優化問題了。
- 雖然各種規劃問題的求解演算法差別很大,但我們所用的程式設計實現方法都是基於 PuLP工具包,程式設計步驟都是一致的。
- 本系列集中體現了與其它課程的區別,沒有展開講演算法的實現步驟,而是重點講程式設計方法的選擇、建立模型方程的過程和程式設計實現的步驟,這主要是為了便於小白學習和掌握。
【本節完】
版權宣告:
歡迎關注『Python小白的數學建模課 @ Youcans』 原創作品
原創作品,轉載必須標註原文連結。
Copyright 2021 Youcans, XUPT
Crated:2021-06-06
歡迎關注 『Python小白的數學建模課 @ Youcans』,每週更新數模筆記
Python小白的數學建模課-01.新手必讀
Python小白的數學建模課-02.資料匯入
Python小白的數學建模課-03.線性規劃
Python小白的數學建模課-04.整數規劃
Python小白的數學建模課-05.0-1規劃
Python小白的數學建模課-A1.國賽賽題型別分析
Python小白的數學建模課-A2.2021年數維杯C題探討
Python小白的數學建模課-A3.12個新冠疫情數模競賽賽題及短評
Python數模筆記-PuLP庫
Python數模筆記-StatsModels統計迴歸
Python數模筆記-Sklearn
Python數模筆記-NetworkX
Python數模筆記-模擬退火演算法