线性规划求解器实现:单纯形法、大M法与两阶段法完整指南

发布时间:2026/10/23 13:20:02
线性规划求解器实现:单纯形法、大M法与两阶段法完整指南
简介这份资源面向学习最优化方法、线性规划课程的高校学生与工程技术人员聚焦单纯形法求解中的大M法与两阶段法编程实现帮助读者理解算法原理并完成课程作业或实际建模求解。压缩包共3个文件均为MATLAB的.m脚本整体约4KB体量轻便包含主程序入口、两阶段法函数与大M法单纯形法函数代码注释详尽、逻辑通俗易懂。主程序负责输入约束方程与目标函数自动调用两阶段法或大M法完成规划求解其中两阶段法的单纯形迭代通过调用大M法函数实现便于对照两种方法的异同。目前已有2547人学习下载适合作为课堂实验、算法复现与排错参考读者可借此掌握单纯形表构建、人工变量处理与迭代终止判断等关键环节并在此基础上迁移到更复杂的线性规划问题求解。1. 从一份 .rar 说起线性规划求解器到底该怎么写很多做运筹学作业或者搞算法落地的开发者第一次碰到线性规划都是直接调库——Python 里 scipy.optimize.linprog 一行搞定MATLAB 里 linprog 也是一行。但真到了要自己实现单纯形法、大M法、两阶段法的时候很多人就卡住了约束里有大于等于、有等式、变量还有符号限制怎么统一成标准型人工变量什么时候加、什么时候该从基里换出去大M的M到底取多大才不会数值翻车这份资源就是冲着这个问题来的。它是一套完整的线性规划求解程序实现覆盖了单纯形法、大M法和两阶段法三条路线适合正在学最优化方法课程、需要交程序作业或者想自己写一个不依赖外部库的 LP 求解器的开发者。拿到手之后你能看到从模型标准化到迭代求解的完整链路而不是一个只会处理小于等于约束的玩具代码。2. 项目结构与环境拿到 .rar 之后先看什么2.1 目录组织与文件职责解压之后常见的组织方式是按算法分文件再配一个统一的入口。我一般会先找主程序文件再看有没有单独的模型输入文件。典型的目录长这样LP-Simplex/ ├── main.py # 统一入口选择算法并读取模型 ├── simplex.py # 标准单纯形法实现 ├── big_m.py # 大M法实现 ├── two_phase.py # 两阶段法实现 ├── standardize.py # 模型标准化转标准型、加松弛/剩余/人工变量 ├── utils.py # 表格打印、基变量追踪、结果格式化 └── models/ ├── model1.txt # 测试用例全小于等于约束 ├── model2.txt # 测试用例含大于等于和等式 └── model3.txt # 测试用例含无界或退化情况这个结构的好处是算法之间互相独立但共享标准化和工具模块。standardize.py 是整个项目的地基因为不管用哪种方法第一步都是把原始模型转成标准型。如果你拿到的版本没有单独拆出这个文件那标准化逻辑大概率嵌在各自算法里改起来会比较痛苦。2.2 运行环境与依赖这套程序通常是 Python 实现依赖极少标准库就能跑。常见做法是用 numpy 做矩阵运算但纯列表也能实现。运行方式# 确认 Python 版本一般 3.7 以上即可 python --version # 安装可选依赖如果代码里用了 numpy pip install numpy # 运行主程序按提示选择算法和模型文件 python main.py如果你的环境里没有 numpy而代码又 import 了它要么装一个要么把矩阵运算改成嵌套列表。我一般会先跑一遍 models 目录下的测试用例确认输出和预期一致再去改代码。2.3 模型输入格式模型文件一般用纯文本描述格式不统一但常见的是这种max 3x1 5x2 s.t. x1 4 2x2 12 3x1 2x2 18 x1, x2 0解析的时候要注意目标函数可能是 max 也可能是 min约束符号有 、、 三种变量可能有非负限制也可能没有。这些差异全部要在标准化阶段处理掉。如果模型文件格式和代码里的解析逻辑对不上程序会直接报解析错误这时候先看 standardize.py 里的正则表达式或者 split 逻辑确认它期望的格式。3. 核心模块拆解标准化、单纯形表与迭代逻辑3.1 模型标准化把所有约束统一成标准型标准型的定义是目标函数求 max所有约束是等式右端项非负所有变量非负。原始模型里但凡有不符合的都要在这一步转过来。def standardize(c, A, b, senses, var_bounds): 将原始 LP 模型转为标准型 c: 目标函数系数列表 A: 约束系数矩阵二维列表 b: 右端项列表 senses: 约束符号列表如 [, , ] var_bounds: 变量符号限制如 [0, free, 0] 返回: 标准型下的 c_std, A_std, b_std, 以及变量映射信息 # 如果目标函数是 min取反转为 max # 如果 b[i] 0该行两边同乘 -1同时翻转不等号方向 # 对 约束加松弛变量对 约束加剩余变量和人工变量 # 对自由变量做 x x - x 的拆分 ...这段逻辑是整个程序最容易出 bug 的地方。参数 senses 和 var_bounds 必须和模型文件里的描述严格对应否则标准化出来的矩阵维度对不上后面单纯形迭代直接崩。我一般会在标准化之后打印一遍 A_std 和 b_std人工核对前两行确认松弛变量和人工变量的列位置正确。3.2 单纯形表迭代进基、出基与最优性判断标准型建好之后单纯形法的核心就是一个循环找进基变量、找离基变量、做枢轴变换、更新表格直到检验数全部非正。def simplex_iterate(tableau, basis, c_std): tableau: 当前单纯形表包含系数矩阵和右端项 basis: 基变量索引列表 c_std: 标准型目标函数系数 返回: 最优解、最优值、迭代次数 while True: # 计算检验数 sigma_j c_j - z_j # 如果所有 sigma_j 0当前基可行解就是最优解 # 否则选 sigma_j 0 中最大的作为进基变量最大化问题 # 对进基列做最小比值检验确定离基变量 # 如果进基列所有系数 0问题无界返回 # 做枢轴变换更新 tableau 和 basis ...检验数的计算方式决定了你能不能正确处理退化情况。常见做法是选最大正检验数进基但如果遇到循环退化可能需要 Bland 规则——选下标最小的正检验数。这个细节在代码里不一定实现但如果你拿到的程序在某些测试用例上死循环大概率就是这个问题。3.3 大M法与两阶段法的差异实现大M法的思路是在目标函数里给人工变量加上一个很大的惩罚系数 M让它们在迭代中尽快被换出基。两阶段法是先最小化人工变量之和如果第一阶段最优值为 0说明找到了可行基再进入第二阶段求解原目标。def big_m_method(c_std, A_std, b_std, artificial_vars): 大M法在目标函数中给人工变量加 -M 系数最大化问题 M 通常取一个远大于所有原始系数的数如 1e6 M 1e6 c_big_m c_std.copy() for idx in artificial_vars: c_big_m[idx] -M return simplex_iterate(build_tableau(A_std, b_std), initial_basis, c_big_m) def two_phase_method(c_std, A_std, b_std, artificial_vars): 两阶段法第一阶段最小化人工变量之和 如果第一阶段最优值 0原问题无可行解 否则用第一阶段的最终基作为第二阶段的初始基 # Phase 1: 构造辅助问题目标函数只含人工变量 # Phase 2: 恢复原目标函数从 Phase 1 的基继续迭代 ...大M法的坑在于 M 的取值。取太小人工变量可能不会被完全换出取太大浮点误差会淹没原始系数。两阶段法没有这个问题但代码量更大需要维护两个阶段的目标函数和表格。我一般建议先用两阶段法验证正确性再用大M法做对比如果两者结果不一致优先信两阶段法。4. 跑通与验证从测试用例到结果解读4.1 用三个典型模型验证程序拿到程序之后不要急着改代码先用自带测试用例跑一遍。我一般会准备三个模型一个全是 约束的简单问题一个含 和 的混合约束问题一个可能出现无界或退化的边界问题。# 依次运行三个测试模型 python main.py --model models/model1.txt --method simplex python main.py --model models/model2.txt --method big_m python main.py --model models/model3.txt --method two_phase运行之后重点看三个输出最优解向量、最优目标值、迭代次数。如果程序还打印了单纯形表可以手动核对第一次迭代的检验数和枢轴元素确认进基出基逻辑没写反。4.2 结果解读与常见输出格式程序输出一般包含这几块输出项含义检查要点最优解决策变量的取值是否满足所有原始约束最优值目标函数在最优解处的值与手动计算或调库结果对比迭代次数单纯形表变换次数异常大可能意味着循环或退化基变量列表最终基中的变量索引人工变量是否已全部换出检验数最终表的 sigma_j是否全部非正如果最优解里还有人工变量取正值说明原问题无可行解这时候要回头检查标准化阶段是不是把约束方向搞反了。4.3 与调库结果交叉验证最稳妥的验证方式是用 scipy 跑一遍同样的模型对比最优值。注意 scipy.optimize.linprog 默认求 min如果你的模型是 max要对目标系数取反。from scipy.optimize import linprog # 原问题 max 3x1 5x2转为 min -3x1 - 5x2 c [-3, -5] A [[1, 0], [0, 2], [3, 2]] b [4, 12, 18] res linprog(c, A_ubA, b_ubb, bounds[(0, None), (0, None)]) print(-res.fun) # 转回 max 的最优值如果两边差很多先检查你的标准化有没有改变问题的可行域再检查单纯形迭代的终止条件是不是写成了「所有检验数 0」而不是「 0」。5. 避坑与排查那些年单纯形法教我的事5.1 人工变量换不出去迭代死循环现象程序跑了几十次迭代还在循环基变量里始终有人工变量。原因大M法的 M 取值不够大或者两阶段法第一阶段没有正确最小化人工变量之和。也可能是退化导致循环进基出基规则没有用 Bland 规则。解决先把 M 调到 1e8 试试如果还不行就换两阶段法。如果是退化循环在进基变量选择时加一个规则当最大正检验数有多个时选下标最小的。5.2 右端项出现负数最小比值检验失效现象标准化之后 b 向量里有负数单纯形表第一列出现负值最小比值算出来是负的。原因标准化阶段没有对 b[i] 0 的行做翻转处理。标准型要求所有右端项非负否则初始基可行解不成立。解决在 standardize 函数里加一步如果 b[i] 0该行所有系数和右端项同乘 -1同时把 变成 变成 。这一步做完再检查一遍 b 是否全部非负。5.3 自由变量拆分后维度对不上现象模型里有自由变量无非负限制标准化之后矩阵列数和变量数对不上索引越界。原因自由变量 x 拆成 x - x 之后变量总数增加了但基变量索引和检验数数组没有同步扩展。解决在标准化函数里维护一个变量映射表记录原始变量和标准型变量的对应关系。所有后续操作都用标准型变量索引最后输出结果时再映射回原始变量。5.4 无界解误判为最优解现象程序输出「找到最优解」但目标值异常大或者解向量里有极端值。原因进基列的系数全部 0 时问题是无界的但代码可能没有正确处理这个分支直接跳出了循环。解决在最小比值检验之前加一个判断如果进基列所有系数 0立即返回「问题无界」不要继续迭代。5.5 浮点误差导致检验数该为零的不为零现象理论上应该等于 0 的检验数程序算出来是 1e-10 或者 -1e-10导致多迭代一轮或者提前终止。原因浮点运算的精度问题尤其是在做枢轴变换时反复乘除。解决在判断检验数是否 0 时加一个容差比如 1e-9。所有和零比较的地方都统一用这个容差不要直接写 0。6. 二次开发把求解器改成你自己的工具6.1 扩展模型输入格式原始程序大概率只支持一种文本格式。如果你想让它读 JSON 或者 Excel最省事的做法是在 main.py 里加一个解析层把外部格式转成 standardize 函数期望的 c、A、b、senses、var_bounds 五个参数。import json def load_model_from_json(path): 从 JSON 文件加载模型返回标准化函数需要的五个参数 with open(path, r, encodingutf-8) as f: data json.load(f) c data[objective][coefficients] # 如果目标是 min这里先不取反留给 standardize 处理 A data[constraints][coefficients] b data[constraints][rhs] senses data[constraints][senses] var_bounds data[variables][bounds] return c, A, b, senses, var_bounds这样改的好处是不动核心算法只加一个适配器。JSON 里可以写清楚目标方向、约束符号、变量范围比纯文本灵活得多。6.2 加入灵敏度分析单纯形表在最优时其实携带了很多信息。最终表的检验数行对应的是对偶变量的值右端项列对应的是资源影子价格。你可以在 simplex_iterate 返回之前把最终表和基变量列表一起返回然后写一个 report 函数输出灵敏度分析。def sensitivity_report(final_tableau, basis, c_std, b_std): 输出对偶变量、影子价格和检验数 dual_vars final_tableau[-1][:-1] # 最后一行是检验数 shadow_prices final_tableau[:-1][-1] # 最后一列是右端项 print(对偶变量检验数:, dual_vars) print(影子价格右端项:, shadow_prices) # 进一步可以计算系数变化范围但需要额外推导这一步不需要改算法只是把中间结果暴露出来。对于做课程作业或者写报告的场景灵敏度分析往往是加分项。6.3 性能优化与数值稳定性如果模型规模变大几百个变量、几百个约束纯 Python 的嵌套列表会非常慢。我一般会做两件事一是把表格运算改成 numpy 矩阵操作二是加入 Bland 规则防止循环。numpy 的广播机制可以让枢轴变换从三重循环变成一行代码速度提升非常明显。import numpy as np def pivot_numpy(tableau, pivot_row, pivot_col): 用 numpy 做枢轴变换比纯 Python 循环快很多 tableau tableau.astype(float) tableau[pivot_row] tableau[pivot_row] / tableau[pivot_row, pivot_col] for i in range(tableau.shape[0]): if i ! pivot_row: tableau[i] tableau[i] - tableau[i, pivot_col] * tableau[pivot_row] return tableau但要注意numpy 的浮点误差和纯 Python 一样存在甚至因为底层 BLAS 的优化可能更隐蔽。所以容差判断不能省该加 1e-9 的地方还是要加。6.4 一个我踩过的坑有一次我把 M 设成了 1e12结果在一个小规模模型上跑出了完全错误的结果。查了半天才发现原始系数只有个位数M 太大之后浮点数的有效位数被吃掉了人工变量的检验数算出来全是零程序以为已经最优了。从那以后我每次用大M法都强制先跑一遍两阶段法做对照两个结果一致才敢用。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取