【數學建模】線性規劃各種問題的Python調包方法

關鍵詞:Python、調包、線性規劃、指派問題、運輸問題、pulp、混合整數線性規劃(MILP)

注:此文章是線性規劃的調包實現,具體步驟原理請搜索具體解法。

??本文章的各個問題可能會采用多種調用方法,為什么?因為這些包各有特點,有些語法特別像matlab,只要稍稍改變即可達成代碼交換;而有些包利用了python本身的特性,在靈活度與代碼的可讀性上更高。我認為這些包各有優劣,各位各持所需吧。

??看了本文章能做到什么?你可以在本文章內學到線性規劃的幾個問題的求解方式,并學會如何用pulp包解決線性規劃問題。無論是整數規劃(Integer Program)、01規劃(Binary Program)還是混合整數線性規劃(MILP),你都可以得到很好的解題方法。

一、線性規劃

該問題引用自《數學建模算法與應用-司守奎》第一章線性規劃 3.線性規劃
包的具體使用可參考scipy官網

??求解最普通的線性規劃問題:\min{z} = 2x_1+3x_2+x_3 \\ \begin{equation} \left\{ \begin{array}{lr} x_1 + 4x_2+2x_3 \geq 8 \\ 3x_1 + 2x_2 \geq 6 \\ x_1,x_2,x_3 \geq 0 \end{array} \right. \end{equation}

scipy調包代碼

import numpy as np

z = np.array([2, 3, 1])

a = np.array([[1, 4, 2], [3, 2, 0]])

b = np.array([8, 6])

x1_bound = x2_bound = x3_bound =(0, None)

from scipy import optimize

res = optimize.linprog(z, A_ub=-a, b_ub=-b,bounds=(x1_bound, x2_bound, x3_bound))

print(res)

#output:
#     fun: 7.0
# message: 'Optimization terminated successfully.'
#     nit: 2
#   slack: array([0., 0.])
#  status: 0
# success: True
#       x: array([0.8, 1.8, 0. ])

??注意,此函數和matlab的linprog有很多相似之處。
??首先默認就是求解最小值,如果想要求最大值,需要對目標函數的各參數取反(既對z取反),并在得出的結果(func)前取反。
??并且所有約束條件都為≤,因此例子中傳入參數是前面都加了負號。
??bound為邊界的二元元組,None時為無邊界。
??如果存在類似x_1+2x_2+4x_3=101這種情況,可以:

A_eq = [[1,2,4]]
b_eq = [101]

并在linprog中傳入。
??得出的結果為scipy.optimize.optimize.OptimizeResult(優化結果)類型,是類似字典的結構,例如想要優化結果代入目標函數的值,可以res.fun或res['fun']這樣取值。

pulp調包代碼

import pulp
#目標函數的系數
z = [2, 3, 1]
#約束
a = [[1, 4, 2], [3, 2, 0]]
b = [8, 6]
#確定最大化最小化問題,最大化只要把Min改成Max即可
m = pulp.LpProblem(sense=pulp.LpMinimize)
#定義三個變量放到列表中
x = [pulp.LpVariable(f'x{i}', lowBound=0) for i in [1,2,3]]
#定義目標函數,lpDot可以將兩個列表的對應位相乘再加和
#相當于z[0]*x[0]+z[1]*x[0]+z[2]*x[2]
m += pulp.lpDot(z, x)

#設置約束條件
for i in range(len(a)):
    m += (pulp.lpDot(a[i], x) >= b[i])
#求解
m.solve()
#輸出結果
print(f'優化結果:{pulp.value(m.objective)}')
print(f'參數取值:{[pulp.value(var) for var in x]}')

#output:
#優化結果:7.0
#參數取值:[2.0, 0.0, 3.0]

??每一步的說明已經注釋在代碼中,可以看到輸出結果,兩者的變量取值并不一致,但代入目標函數的結果都是一樣的。
??同樣的,如果存在類似x_1+2x_2+4x_3=101這種情況,可以:

A_eq = [1,2,4]
b_eq = 101
m += (pulp.lpDot(A_eq, x) == b_eq)

二、運輸問題

?? 某商品有m個產地、n個銷地,各產地的產量分別為a_1, ..., a_m,各銷地的 需求量分別為b_1,...,b_n。若該商品由i產地運到j銷地的單位運價為c_{ij},問應該如何調 運才能使總運費最省?
?? 引入變量x_{ij},其取值為由i產地運往j銷地的該商品數量,數學模型為 :\Large \min {\sum_{i=1}^m\sum_{j=1}^{n}c_{ij}x_{ij}} \\ \left\{ \begin{array}{lr} \Large \sum_{j=1}^n x_{ij} = a_i,\quad i=1,...,m\\ \Large \sum_{i=1}^m x_{ij} = b_j, \quad j=1,2,..., n \\ \Large x_{ij} \geq 0 \end{array} \right.
例題:

pulp調包代碼

import pulp
import numpy as np
from pprint import pprint

def transportation_problem(costs, x_max, y_max):

    row = len(costs)
    col = len(costs[0])

    prob = pulp.LpProblem('Transportation Problem', sense=pulp.LpMaximize)

    var = [[pulp.LpVariable(f'x{i}{j}', lowBound=0, cat=pulp.LpInteger) for j in range(col)] for i in range(row)]

    flatten = lambda x: [y for l in x for y in flatten(l)] if type(x) is list else [x]

    prob += pulp.lpDot(flatten(var), costs.flatten())

    for i in range(row):
        prob += (pulp.lpSum(var[i]) <= x_max[i])

    for j in range(col):
        prob += (pulp.lpSum([var[i][j] for i in range(row)]) <= y_max[j])

    prob.solve()

    return {'objective':pulp.value(prob.objective), 'var': [[pulp.value(var[i][j]) for j in range(col)] for i in range(row)]}

然后構造參數傳遞進去:

if __name__ == '__main__':
    costs = np.array([[500, 550, 630, 1000, 800, 700],
                       [800, 700, 600, 950, 900, 930],
                       [1000, 960, 840, 650, 600, 700],
                       [1200, 1040, 980, 860, 880, 780]])

    max_plant = [76, 88, 96, 40]
    max_cultivation = [42, 56, 44, 39, 60, 59]
    res = transportation_problem(costs, max_plant, max_cultivation)

    print(f'最大值為{res["objective"]}')
    print('各變量的取值為:')
    pprint(res['var'])

#output:
#最大值為284230.0
#各變量的取值為:
#[[0.0, 0.0, 6.0, 39.0, 31.0, 0.0],
# [0.0, 0.0, 0.0, 0.0, 29.0, 59.0],
# [2.0, 56.0, 38.0, 0.0, 0.0, 0.0],
# [40.0, 0.0, 0.0, 0.0, 0.0, 0.0]]

三、指派問題

該問題引用自《數學建模算法與應用-司守奎》第一章線性規劃 3.指派問題
調包解決方法參考https://blog.csdn.net/your_answer/article/details/79160045
可參考官網https://docs.scipy.org/doc/scipy-0.18.1/reference/generated/scipy.optimize.linear_sum_assignment.html

??擬分配n人去干n項工作,沒人干且僅干一項工作,若分配第i人去干第j項工作,需花費c_{ij}單位時間,問應如何分配工作才能使公認花費的總時間最少?
假設指派問題的系數矩陣為:C= \left[ \begin{matrix} 12 & 7&9&7&9\\ 8&9&6&6&6 \\ 7&17&12&14&12\\ 15&14&6&6&10\\ 4&10&7&10&6 \end{matrix} \right]??引入變量x_{ij},若分配ij工作,則取x_{ij}=1,否則取x_{ij}=0,上述指派問題的數學模型為\min {\sum_{i=1}^{n} {\sum_{j=1}^{n}{c_{ij}x_{ij}}}} \\ s.t. \sum_{j=1}^{n}x_{ij}=1 \\ \qquad \sum_{i=1}^{n}x_{ij}=1 \\ \qquad x_{ij}=0或1??指派問題的可行解用矩陣表示,每行每列有且只有一個元素為1,其余元素均為0。

調用scipy解決

??原書使用匈牙利算法解決的,在這里我們用scipy的優化模塊解決

import numpy as np
from scipy.optimize import linear_sum_assignment

??引入包,linear_sum_assignment是專門用于解決指派問題的模塊。

efficiency_matrix = np.array([
    [12,7,9,7,9],
    [8,9,6,6,6],
    [7,17,12,14,12],
    [15,14,6,6,10],
    [4,10,7,10,6]
])

row_index, col_index=linear_sum_assignment(efficiency_matrix)
print(row_index+1)
print(col_index+1)
print(efficiency_matrix[row_index,col_index])

#output:
#[1 2 3 4 5]
#[2 3 1 4 5]
#[7 6 7 6 6]

??定義了開銷矩陣(指派問題的系數矩陣)efficiency_matrix,傳入linear_sum_assignment,結果返回的是最優指派的行和列,例如第一行選擇第二列,意為:將第一個人派往第二個工作。而根據numpy.array的性質,傳入行和列就會返回行列所對應的值,即為輸出的第三列

print(efficiency_matrix[row_index, col_index].sum())
#output:
# 32

??對其求和,即可得到指派問題的最小時間。

調用pulp解決

??先定義通用解決方法,其中的flatten是遞歸展開列表用的。

def assignment_problem(efficiency_matrix):
    row = len(efficiency_matrix)
    col = len(efficiency_matrix[0])

    flatten = lambda x: [y for l in x for y in flatten(l)] if type(x) is list else [x]

    m = pulp.LpProblem('assignment', sense=pulp.LpMinimize)
    var_x = [[pulp.LpVariable(f'x{i}{j}', cat=pulp.LpBinary) for j in range(col)] for i in range(row)]

    m += pulp.lpDot(efficiency_matrix.flatten(), flatten(var_x))

    for i in range(row):
        m += (pulp.lpDot(var_x[i], [1]*col) == 1)

    for j in range(col):
        m += (pulp.lpDot([var_x[i][j] for i in range(row)], [1]*row) == 1)

    m.solve()

    print(m)

    return {'objective':pulp.value(m.objective), 'var': [[pulp.value(var_x[i][j]) for j in range(col)] for i in range(row)]}

??然后定義矩陣,輸入求解

efficiency_matrix = np.array([
    [12, 7, 9, 7, 9],
    [8, 9, 6, 6, 6],
    [7, 17, 12, 14, 9],
    [15, 14, 6, 6, 10],
    [4, 10, 7, 10, 9]
])

res = assignment_problem(efficiency_matrix)
print(f'最小值{res["objective"]}')
print(res['var'])

#output
#最小值32.0
#[[0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 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]]

四、pulp的使用方式

基本使用

??可以看出,pulp在線性規劃這一部分,有更多的通用性,編寫程序更自由。前面的例子已經足夠豐富了,但是如果讀到這里還不去清楚pulp的使用方式的話……可以去百度一下,我這里也簡單講一講。

??首先是定義一個問題,第一個參數為問題的名稱,不過并非必要的參數,而通過sense參數可決定優化的是最大值還是最小值

prob = pulp.LpProblem('problem name', sense=pulp.LpMinimize)

??然后是定義變量:

x0 = pulp.LpVariable('x0', lowBound=0, upBound=None, cat=pulp.LpInteger)
x1 = pulp.LpVariable('x1', lowBound=0, upBound=None, cat=pulp.LpInteger)
x2 = pulp.LpVariable('x2', lowBound=0, upBound=None, cat=pulp.LpInteger)

??這里定義了三個變量,第一個參數為變量名,lowBound 、upBound為上下限,cat為變量類型,默認為連續變量,還可以設為離散變量或二值變量,具體怎么設置?
如上述代碼所示,pulp.LpInteger為離散變量,pulp.LpBinary為二值變量,同時也可以傳入'Integer'字符串的方式指明變量類型。從上面幾個問題的代碼可以看出,我幾乎沒有單個定義變量,而是批量定義的。
??然后是添加目標函數:

prob += 2*x0-5*x1+4*x2

??只要讓問題(prob)直接加變量的表達式即可添加目標函數。
??再之后是添加約束:

prob += (x0+x1-6*x2 <= 120)

??只要讓問題(prob)直接加變量的判斷式即可添加約束

prob.solve()

??調用solve方法解出答案,如果省去這一步,后面的變量和結果值都會顯示None。

print(pulp.value(prob.objective))
print(pulp.value(x0))

打印優化結果,并顯示當優化達到結果時x0的取值。

思考程序本質

??problem對象是如何通過不斷加來獲得目標函數和約束的?熟悉python或者c++的朋友可能會想到一個詞:操作符重載。
??沒錯,就是這么實現的,上述的對象幾乎都實現了不同的重載。
??首先是Problem對象prob,全名pulp.pulp.LpProblem;當打印輸出(print)時,會打印出問題名,當不斷增加目標函數、約束時,也會隨著print輸出;而它的__add__一定是被定義過了,我們先說其他對象。
??當我們定義一個變量時,它的類型是pulp.pulp.LpVariable,當我們對這些變量和其他變量做加法、和其他常數做乘法時,它會返回一個新的對象,經檢測,這個新對象的類名叫pulp.pulp.LpAffineExpression,顧名思義,叫做關系表達式;如果print,會打印這個關系表達式。
??而如果對關系表達式做出:>=、<=、==時,會返回新的對象,類名叫做pulp.pulp.LpConstraint,即約束對象;如果print,會打印這個約束。
??將關系表達式(pulp.pulp.LpAffineExpression)與問題(pulp.pulp.LpProblem)相加時,會返回新的問題對象,并且新的問題對象會將這個關系表達式作為目標函數。
??將約束對象(pulp.pulp.LpConstraint)與問題(pulp.pulp.LpProblem)相加時,會返回新的問題對象,并且這個新的問題對象會多出約束對象所代表的約束條件。
??調用問題對象的solve方法,解出線性規劃的解。
??訪問問題對象的objective成員變量,會得到目標函數(關系表達式對象)。
??調用pulp的value方法,將獲得對變量代入值的結果,如果是關系表達式對象,將獲得優化結果;如果是變量對象,將獲得優化結果達到時的變量取值;如果是None,說明你忘調用solve了。

??這個包的使用,我是在google中查到的,如果有興趣,可以去原地址看看,擁有更多的應用以及其他包的調用https://qiita.com/SaitoTsutomu/items/bfbf4c185ed7004b5721
??也可以去bilibili看看相關的視頻https://www.bilibili.com/video/av18818604?from=search&seid=17463693264345909709

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容