
1. 從“分蛋糕”到“做決策”整數規劃到底是什么如果你曾經遇到過這樣的問題公司有5個項目但預算只夠啟動其中3個怎么選才能讓總收益最大或者物流中心需要向10個城市送貨每輛車的裝載量有限如何安排最少的車輛跑完所有路線再或者學校排課如何把有限的教室和老師在互不沖突的時間段里安排給不同的班級這些問題背后都有一個共同的數學靈魂在起作用——那就是整數規劃。簡單來說整數規劃是數學規劃的一個分支。你可以把它想象成我們熟悉的“線性規劃”加上了“整數”的緊箍咒。在線性規劃里你的決策變量比如生產多少產品、分配多少資源可以是任意實數比如生產3.14臺機器、分配2.5噸原料這在數學上完全可行。但在現實世界里很多決策必須是整數你不能雇傭半個員工不能購買半架飛機不能開設半家門店。當這些決策變量被強制要求取整數值0, 1, 2, 3...時線性規劃就升級成了整數規劃。其中有一種特例極為常見且強大那就是0-1整數規劃。這里的變量只能取0或1代表了“是”或“否”、“選”或“不選”的二元決策。文章開頭提到的項目選擇問題就可以為每個項目定義一個0-1變量選這個項目變量就等于1不選就等于0。這種模型天生就是為了處理“選擇”、“指派”、“覆蓋”這類離散決策而生的。所以整數規劃的核心價值就在于它將數學的嚴謹性與現實的離散性橋接了起來。它不滿足于給出一個“理論上最優但現實中無法執行”的分數解而是執著地尋找那個“現實中可行且數學上最優”的整數解。從芯片設計中的電路布局、航空公司機組排班、金融領域的投資組合優化選擇哪些股票、買多少手手數必須是整數到能源系統的發電機組啟停調度整數規劃的身影無處不在。它是一位沉默的“最優解架構師”在無數個離散的可能性中為我們找出那條最經濟的路徑。2. 核心武器庫三類經典整數規劃模型與建模心法理解了整數規劃是什么接下來就要看看我們手里有哪些趁手的“模型武器”。不同的現實問題對應著不同結構的數學模型。掌握這幾類經典模型及其建模思路是把你遇到的實際問題“翻譯”成數學語言的關鍵第一步。2.1 背包問題資源約束下的最優選擇這是最直觀的一類問題。想象你有一個容量有限的背包面前有一堆物品每個物品有自己的價值和重量。你的目標是在不超過背包容量的前提下選出總價值最高的一組物品。這就是經典的0-1背包問題。建模示例公司有1000萬研發預算有5個潛在項目。項目i預計收益為p_i萬元所需研發投入為c_i萬元。是否投資項目i用0-1變量x_i表示。決策變量x_i 1表示選擇項目ix_i 0表示不選。目標函數最大化總收益Max Z Σ(p_i * x_i)。約束條件總投入不能超預算Σ(c_i * x_i) 1000。注意這是最基礎的背包模型。實際問題中約束可能更復雜比如項目間存在依賴關系選了A才能選B這可以通過添加額外的約束來實現例如x_B x_A。2.2 指派問題如何實現最佳匹配這類問題關注的是如何將一系列“任務”最有效地分配給一系列“執行者”通常是一對一的分配。比如將不同的工作分配給不同的機器每臺機器做一項工作或者將不同的客戶分配給不同的銷售代表。建模示例有4項任務J1-J4和4名員工E1-E4。員工i完成任務j所需的時間或成本為c_{ij}。目標是找到一種分配方案使總耗時或總成本最小且每項任務有且僅有一名員工負責每名員工也僅負責一項任務。決策變量x_{ij} 1表示將任務j分配給員工i否則為0。目標函數最小化總成本Min Z ΣΣ(c_{ij} * x_{ij})。約束條件每個任務必須被分配一次對每個任務jΣ_i x_{ij} 1。每個員工必須被分配一個任務對每個員工iΣ_j x_{ij} 1。指派問題是整數規劃中結構非常特殊的一類它有高效的專用算法如匈牙利算法但用通用整數規劃求解器同樣可以解決。2.3 集合覆蓋與選址問題用最少的點覆蓋最大的面這類問題在物流、公共服務領域極為常見。目標是選擇最少數量的“設施點”如倉庫、消防站、5G基站使得所有“需求點”如客戶小區、城市街區都能在一定的服務半徑內被至少一個設施點覆蓋。建模示例某市計劃新建急救中心有8個候選地點。需要確保全市15個主要街區中每個街區在10公里范圍內至少有一個急救中心。已知每個候選地點能覆蓋哪些街區覆蓋關系可用一個0-1矩陣表示。目標是使建設的急救中心數量最少。決策變量y_j 1表示在候選地點j建設急救中心否則為0。目標函數最小化建設總數Min Z Σ y_j。約束條件對每個街區i它必須被至少一個已建設的中心覆蓋。即對所有能覆蓋街區i的候選地點j至少有一個y_j 1。用數學表達對每個街區iΣ_{j ∈ S_i} y_j 1其中S_i是能覆蓋街區i的所有候選地點集合。選址問題變體很多比如加上建設成本不同、需求點權重人口不同等但核心的覆蓋思想不變。2.4 建模心法從現實到模型的“翻譯”藝術把實際問題變成數學模型是最考驗功力的環節。這里有幾個我總結的心得定義變量是關鍵的第一步首先要問自己需要做出哪些決策這些決策中哪些必須是整數通常選擇、數量、順序、指派關系都需要用整數變量來刻畫。對于數量用一般整數變量對于是否用0-1變量。目標要單一且可量化目標函數通常是最小化成本、時間、距離或者最大化利潤、覆蓋率、滿意度。確保目標是能用決策變量的線性組合表達的。如果有多目標需要將其轉化為單目標例如使用加權和法或者將其中一個目標設為約束。約束是現實的鐐銬仔細梳理所有限制條件資源上限錢、人、時間、邏輯關系如果A則BA和B不能同時選、物理規律產能限制、政策要求最低服務標準等。每一個條件都要轉化為一個或多個線性不等式或等式。善用0-1變量表達復雜邏輯這是建模中最巧妙的部分。例如固定成本問題如果生產某種產品需要先支付一筆固定設備費無論生產多少然后再按單位成本計費??梢砸胍粋€0-1變量y表示是否生產和一個連續變量x表示產量。約束可以寫成x M * y其中M是一個很大的數Big-M法。這樣如果y0則x必須為0如果y1則x可以在一個合理范圍內取值。目標函數中則加上固定成本項f * y?;コ膺x擇項目A和項目B至多選一個。約束x_A x_B 1。依賴關系選項目B必須先選項目A。約束x_B x_A。把現實世界的復雜關系用簡潔的數學不等式編織起來這個過程本身就充滿了美感與挑戰。3. 算法內功分支定界法是如何“抽絲剝繭”找最優的模型建好了扔給求解器一會兒就出結果了。但你知道求解器在背后經歷了怎樣一場“頭腦風暴”嗎理解最核心的求解算法——分支定界法不僅能讓你在結果異常時有所洞察更能提升你建模的“算法友好性”。別被名字嚇到我們可以用一個簡單的例子來還原這個過程。假設我們有一個最大化問題的整數規劃只有兩個整數變量x1和x2。第一步放松先求個“天花板”首先算法會暫時“忘記”變量必須是整數的要求把它當作一個普通的線性規劃問題來求解。這個解稱為線性松弛解。因為約束變少了去掉了整數要求這個解的目標函數值比如利潤一定不低于原整數問題的最優值。它為我們提供了一個最優值的“上界”對于最大化問題就像一個理想中的“天花板”。但這個解很可能不是整數解比如x12.5, x23.7。第二步分支給非整數變量“做選擇”既然x12.5不是整數我們就必須做出選擇在最終的整數解里x1要么2要么3。它不可能在2和3之間。于是我們把原問題**分解分支**成兩個子問題子問題1在原問題基礎上增加約束x1 2。子問題2在原問題基礎上增加約束x1 3。 這樣我們就把那個討厭的非整數解x12.5從這兩個子問題的可行域中排除出去了。這兩個子問題覆蓋了原問題所有可能的整數解且互不重疊。第三步定界與剪枝高效排除“差生”對每個新生成的子問題我們繼續求解它的線性松弛問題。這時會出現幾種情況松弛問題無解那么這個子問題下肯定也沒有整數解整個分支可以剪掉丟棄。松弛解是整數解太棒了我們找到了原問題的一個可行整數解。它的目標值記為我們目前找到的“下界”對于最大化問題也就是我們目前掌握的“地板”。松弛解的目標值 當前下界即使這個子問題未來能找到整數解其目標值也不會比我們已經掌握的“地板”更好了對于最大化問題。那么這個分支也沒有繼續探索的價值剪掉。松弛解非整數且目標值 當前下界這個分支還有潛力可能藏著更好的整數解。我們把它放回待探索列表等待后續繼續對它進行分支比如再選一個非整數變量x2來分。第四步迭代直到“天花板”碰到“地板”算法會不斷地從待探索列表中選取一個子問題進行分支、求解松弛、定界和剪枝。這個過程就像在一棵不斷生長的決策樹上進行搜索。上界所有待探索子問題的松弛解目標值中最大的那個就是全局上界。下界我們目前找到的所有可行整數解中目標值最好的那個就是全局下界。隨著搜索進行上界會不斷下降因為分支增加約束下界會不斷上升因為找到更好的整數解。當全局上界和全局下界的差距縮小到我們設定的精度范圍內時搜索就可以停止了。此時我們持有的那個“下界”對應的整數解就是問題的最優解或非常接近最優。為什么這個方法有效它避免了暴力枚舉所有可能的整數組合那是指數級爆炸的。通過求解松弛問題它能快速評估一個分支的“潛力”上界并及時把沒有希望的分支剪掉極大地縮小了搜索范圍。這就像在迷宮中你總是先站在高處松弛解望一眼如果這條路盡頭的房間上界看起來還不如你手里已經找到的寶藏下界好那這條岔路根本就不用進去搜了。在實際使用求解器時我們雖然不直接操控這個過程但理解它有助于我們解讀日志當求解器輸出“Gap 0.01%”時你就知道它已經找到了一個解并且證明了不存在比它好過0.01%的其他解。優化模型一個“緊”的線性松弛即松弛解離整數解很近能極大地加速求解。這意味著你的模型 formulation 很好分支定界效率會很高。處理超時如果時間有限可以設置一個 Gap 容忍度比如5%讓求解器找到一個“足夠好”的解就停止而不必追求絕對最優。4. 實戰用PythonPuLP求解一個投資組合問題理論說得再多不如親手跑一遍代碼來得實在。我們用一個具體的投資組合優化問題來演示如何從建模到求解走完全流程。我們將使用Python和一個非常友好的優化建模庫——PuLP。問題描述假設你是一個投資者有100萬資金。市場上有5支股票S1-S5可供選擇。每支股票有一個預期的年化收益率r_i一個風險系數risk_i這里為簡化假設風險可用系數量化以及一個最低起購金額min_i。此外出于分散風險考慮你規定最多選擇3支股票。如果選擇了高風險risk_i 5的股票S2則必須同時選擇一支低風險risk_i 3的股票S1作為對沖。每支股票的投資金額必須是其最低起購金額的整數倍。我們的目標是在滿足上述所有約束的前提下最大化投資組合的預期總收益。步驟1環境準備與數據定義首先確保安裝了pulp庫pip install pulp。然后我們定義問題數據。import pulp # 定義股票數據 (名稱 收益率% 風險系數 最低起購金額-萬元) stocks { S1: {return: 8, risk: 2, min_lot: 10}, S2: {return: 15, risk: 6, min_lot: 20}, S3: {return: 10, risk: 4, min_lot: 15}, S4: {return: 12, risk: 5, min_lot: 25}, S5: {return: 9, risk: 3, min_lot: 30}, } total_capital 100 # 總資金 100萬元 max_stocks_to_choose 3步驟2建立整數規劃模型我們用PuLP來聲明問題、變量、目標和約束。# 1. 創建問題實例指定為最大化問題 prob pulp.LpProblem(Stock_Investment_Portfolio, pulp.LpMaximize) # 2. 定義決策變量 # 變量 x_i: 是否選擇股票i (0-1變量) x {i: pulp.LpVariable(fx_{i}, catBinary) for i in stocks} # 變量 y_i: 購買股票i的份數 (正整數變量份數投資金額/最低起購金額) y {i: pulp.LpVariable(fy_{i}, lowBound0, catInteger) for i in stocks} # 3. 定義目標函數最大化總收益 # 總收益 Σ (收益率 * 最低起購金額 * 購買份數) prob pulp.lpSum([stocks[i][return]/100.0 * stocks[i][min_lot] * y[i] for i in stocks]) # 4. 定義約束條件 # 4.1 資金約束總投資額不超過總資金 prob pulp.lpSum([stocks[i][min_lot] * y[i] for i in stocks]) total_capital # 4.2 邏輯約束只有選擇了股票i (x_i1)才能購買它 (y_i1)。同時購買份數不能超過一個很大的數M這里用資金上限估算 M total_capital // min([data[min_lot] for data in stocks.values()]) # 一個足夠大的整數 for i in stocks: prob y[i] M * x[i] # 如果x_i0, 則y_i必須為0如果x_i1, y_i可以M prob y[i] 1 * x[i] # 如果x_i1, 則y_i至少為1份 # 4.3 選擇股票數量上限 prob pulp.lpSum([x[i] for i in stocks]) max_stocks_to_choose # 4.4 邏輯依賴約束如果選擇高風險S2 (x_S21)則必須選擇低風險S1 (x_S11) prob x[S2] x[S1] # 4.5 可選每支股票購買份數上限例如不超過5份 for i in stocks: prob y[i] 5步驟3求解并分析結果現在我們把問題丟給求解器PuLP會自動調用它找到的求解器如CBC。# 求解問題 solver pulp.PULP_CBC_CMD(msgFalse, timeLimit30) # 安靜模式最多計算30秒 prob.solve(solver) # 打印求解狀態 print(f求解狀態: {pulp.LpStatus[prob.status]}) print(f最大預期總收益萬元: {pulp.value(prob.objective):.2f}) print(\n最優投資方案) print(- * 40) total_investment 0 for i in stocks: if pulp.value(x[i]) 0.5: # 判斷是否選擇了該股票 lots int(pulp.value(y[i])) investment lots * stocks[i][min_lot] total_investment investment expected_return investment * stocks[i][return] / 100.0 print(f股票 {i}: 購買 {lots} 份 投資額 {investment} 萬元 預期收益 {expected_return:.2f} 萬元) print(- * 40) print(f總投資額: {total_investment} 萬元) print(f資金利用率: {total_investment/total_capital*100:.1f}%)步驟4解讀輸出與模型調整運行上述代碼你可能會得到類似如下的輸出求解狀態: Optimal 最大預期總收益萬元: 10.80 最優投資方案 ---------------------------------------- 股票 S1: 購買 3 份 投資額 30 萬元 預期收益 2.40 萬元 股票 S2: 購買 2 份 投資額 40 萬元 預期收益 6.00 萬元 股票 S5: 購買 1 份 投資額 30 萬元 預期收益 2.70 萬元 ---------------------------------------- 總投資額: 100 萬元 資金利用率: 100.0%解讀與心得結果分析求解器找到了最優解。它選擇了S1, S2, S5三支股票正好用滿100萬資金達到了約束上限。選擇S2高風險高收益的同時按規則選擇了S1低風險進行對沖。S5作為中低風險收益的補充。PuLP使用技巧LpVariable的cat參數是關鍵‘Binary’表示0-1變量‘Integer’表示一般整數變量。用Big-M法y[i] M * x[i]來關聯連續整數變量和0-1變量是標準操作。M需要選得足夠大以保證當x[i]1時y[i]能取到所有可能值但又不能太大否則會影響求解的數值穩定性。通常取一個合理的上界如本例中用總資金估算。約束可以直接用添加到問題對象prob上非常直觀。如果求解失敗或結果奇怪檢查模型是否可行可能約束條件太嚴格互相沖突導致沒有解。可以嘗試逐步放松約束來排查。檢查Big-M的值如果M太小可能會錯誤地截斷可行解如果M太大比如1e9可能會帶來數值計算問題導致求解器性能下降或結果不精確。查看求解狀態pulp.LpStatus[prob.status]如果是Infeasible不可行說明約束矛盾如果是Unbounded無界說明目標函數可以無限大可能漏掉了關鍵約束。調整求解器或參數對于復雜問題可以嘗試換用更強大的商業求解器如Gurobi, CPLEX或在PuLP中設置更長的求解時間、更小的容忍間隙Gap。通過這個完整的例子你應該能感受到將一個問題用代碼“描述”出來然后讓求解器替你完成復雜的搜索計算是一件多么高效且有成就感的事情。PuLP這樣的庫大大降低了優化建模的門檻讓你能更專注于問題本身而非算法實現。5. 避坑指南整數規劃建模與求解中的常見“雷區”走過前面的路你可能已經摩拳擦掌準備用整數規劃大干一場了。但別急這條路雖然強大卻也布滿了新手容易踩進去的坑。下面這些是我和許多同行用時間和頭發換來的經驗教訓希望能幫你繞開它們。5.1 模型不可行當約束變成“死結”這是最常見也最令人頭疼的問題之一你興沖沖地建好模型點擊求解結果求解器直接返回“Infeasible”不可行。這意味著在你的所有約束條件下不存在任何一個解能滿足所有要求。排查思路像偵探一樣思考逐條約束檢查法這是最笨但最有效的方法。暫時注釋掉所有約束然后一條一條地加回去每加一條就求解一次。當某條約束加入后問題突然變得不可行那么這條約束或者它與之前約束的組合就是“罪魁禍首”。松弛法嘗試放寬一些你覺得“可能太嚴”的約束。例如把改成或把苛刻的整數約束暫時改為連續約束。如果放寬后問題變得可行那么你就找到了沖突的源頭。尋找IIS不可行沖突集高級求解器如Gurobi、CPLEX通常提供IIS功能。當問題不可行時它可以幫你找出一組最小的、互相沖突的約束。這就像編譯器報錯時指向具體的代碼行能極大提升調試效率。在PuLP中調用商業求解器時也可以嘗試獲取此信息。檢查數據錯誤很多時候不可行不是模型邏輯問題而是數據輸入錯誤。比如某個資源的需求量被誤輸入為遠大于供應量或者兩個互斥的選項被邏輯關系強制要求同時選中。仔細核對數據特別是單位是否統一。提示在建模初期不要一次性把約束寫得太“死”??梢韵葮嫿ㄒ粋€核心的、寬松的模型確保它能出解。然后再逐步添加復雜的業務規則約束并觀察每次添加對解的影響。這是一種“增量建模”的安全策略。5.2 “維度災難”問題規模與求解時間爆炸整數規劃是NP-Hard問題這意味著在最壞情況下求解時間隨著問題規模變量數、約束數呈指數級增長。你可能建了一個看起來不錯的模型但一求解卻發現“卡死”了幾個小時都沒結果。應對策略簡化模型減少0-1變量能否用連續變量或一般整數變量替代部分0-1變量例如如果某個數量范圍不大直接用整數變量可能比用多個0-1變量組合表示更高效。收緊線性松弛好的模型公式其線性松弛的解應該很接近整數最優解??梢試L試添加一些“有效不等式”來收緊可行域幫助分支定界法更快剪枝。這需要一些經驗和技巧。聚合約束有時多個細粒度的約束可以合并成更緊湊的表達式減少約束數量。利用問題特殊結構你的問題是否屬于某一類特殊問題如指派問題、旅行商問題、背包問題對于這些經典問題可能存在比通用整數規劃更高效的專用算法或啟發式算法。先用專用算法求一個優質解再作為初始解喂給整數規劃求解器也能加速求解。調整求解器參數與接受近似解設置時間限制對于大規模問題追求絕對最優解可能不現實。設置一個合理的求解時間上限如300秒。設置容忍間隙告訴求解器找到一個與最優解差距在1%或5%以內的解就可以停止了。這在很多業務場景下是完全可接受的。提供初始解如果你能通過經驗或簡單規則構造一個可行的初始解將其提供給求解器可以大大縮短求解時間。分解與分層對于超大規模問題可以考慮將其分解成若干個子問題分別求解或者采用分層優化的思路先進行粗粒度的決策再進行細粒度的優化。5.3 數值穩定性當“Big-M”變成“Big Trouble”在建模中我們經常使用“Big-M”法來處理邏輯條件如前文關聯x和y的約束。這個M如果選得不好會帶來嚴重的數值問題。問題如果M設置得過大比如1e9而模型中的其他系數是正常量級如1 100會造成約束矩陣的數值比例失衡。這可能導致求解器內部的數值計算出現舍入誤差輕則求解速度變慢重則找到錯誤的“最優解”或者將可行問題誤判為不可行。黃金法則為每個使用Big-M的約束選擇盡可能小但足夠大的M。如何估算“足夠大”M需要保證當邏輯條件激活時如x1關聯的變量如y能取到其所有可能的最大值。這個最大值應該來自問題的物理或業務意義。示例在前面的投資問題中y[i]購買份數的最大值是多少它受總資金和最小起購金額限制max(y[i]) total_capital / min_lot。因此我們可以為每個股票i單獨設置M_i total_capital // stocks[i][min_lot]這比使用一個全局的巨大M要精確得多。更好的方法如果可能盡量避免使用Big-M。有時可以通過重構模型來消除它。例如某些條件邏輯可以通過添加額外的輔助變量和約束來表達而不依賴一個很大的M。5.4 忽略對稱性求解器在“原地打轉”對稱性是指模型存在多個本質上相同的最優解。例如在一個選址問題中如果所有候選地點成本相同、覆蓋能力相同那么選擇地點A、B、C的方案與選擇地點D、E、F的方案在目標函數值上是完全一樣的。這種對稱性會導致分支定界樹急劇膨脹因為求解器會浪費大量時間去探索這些等價的解空間。識別與處理識別對稱性觀察你的決策變量。如果交換其中一組變量的值問題的所有約束和目標函數值保持不變那么就存在對稱性。打破對稱性添加一些額外的約束來消除對稱解。例如在上述選址例子中我們可以強制要求如果選擇了某個數量的設施那么優先選擇編號小的地點??梢蕴砑蛹s束x_i x_{i1}對于按某種順序排列的候選點。這樣解就被“固定”在一種特定的排列上從而消除了對稱性能顯著提升求解速度。5.5 誤讀結果整數解 vs. 松弛解這是概念理解上的一個坑。初學者有時會困惑為什么我求整數規劃得到的目標值比如最大利潤100萬比直接忽略整數約束求線性規劃得到的目標值比如105萬要差是不是求解器出錯了完全正常這正是整數規劃的本質。線性松弛解因為放松了約束所以提供了一個更“樂觀”對于最大化問題的估計即最優值的上界。整數規劃的解必須滿足所有整數約束可行域更小所以最優值自然可能變差。這個差距被稱為“整數間隙”。建模和求解的藝術正是在于如何縮小這個間隙用可執行的整數方案去盡可能逼近那個理想中的“天花板”。踩過這些坑你會對整數規劃有更深刻的理解。它不僅僅是一個點擊求解就完事的黑箱而是一個需要你精心設計模型、耐心調試參數、理性看待結果的系統工程。每一次對“Infeasible”的排查每一次對求解時間的優化都是你作為建模者成長的印記。