简介:本资源是一套面向高校运筹学、最优化方法课程学习者及MATLAB初学者的线性规划算法实践工具包,聚焦大M法与两阶段法两大经典单纯形扩展技术的程序化实现。资源包含3个功能完备的MATLAB源文件(.m格式),其中main.m为主控入口,twophase.m封装两阶段单纯形求解逻辑,MySimplex_method.m则统一实现大M法与标准单纯形法,并被前者调用,三者协同构成可验证、可调试、可拓展的完整求解框架。所有函数均配有中文注释,输入格式规范,支持用户直接编辑约束条件与目标函数后一键运行。压缩包仅4KB,轻量易用,无冗余文件。目前已有2545人学习下载,适合用于课堂实验验证、课设编程参考、算法原理可视化理解及期末复习实操训练,是衔接理论推导与编程实现的优质教学辅助材料。
1. 大M法和两阶段法不是“选哪个”,而是“什么时候必须用哪个”:线性规划退化、无可行解、人工变量残留的实战判据
你写完单纯形表,迭代五轮后基变量里还卡着两个人工变量,目标函数值卡在 -∞ 附近震荡;或者刚列完初始单纯形表,发现约束右端项有负数,强行加人工变量后,大M项把原目标函数彻底淹没——这不是代码写错了,是建模阶段就埋了雷。这个标题里的“大M法和两阶段法程序实现”,本质是在解决标准单纯形法无法启动或中途失效的三类硬伤:约束不满足非负要求(b_i < 0)、原始问题无可行解、基变量中人工变量无法出基。它不是教科书里并列的两种可选项,而是工程落地时必须按诊断逻辑分流的两条路径:当人工变量在最优解中取正值,说明原问题无可行解,此时两阶段法的第二阶段根本不会执行;而大M法若在最终表中仍含M项残余,你得回溯检查M是否真够大、是否所有含M的行都已消去。本篇面向的是正在调试运筹学课程设计、供应链资源分配模型或电力调度仿真系统的开发者——你手头已有LP建模能力,但卡在“明明公式对,程序跑不出合理解”的黑匣子阶段。我们不讲凸集几何,只拆清:何时该切两阶段、M值怎么设才不翻车、人工变量出基失败时看哪三行、以及为什么Python SciPy的linprog默认不用这两种方法。
2. 从数学定义到程序结构:为什么必须手写大M/两阶段,而不是调用现成求解器
2.1 单纯形法失效的三大临床症状与对应疗法
标准单纯形法要求问题已化为标准型:
- 所有约束为等式(=)
- 所有变量 ≥ 0
- 所有右端常数 b_i ≥ 0
但现实建模中,这三条几乎必然被突破。我们按故障现象反推技术选型:
| 现象 | 数学本质 | 标准单纯形法能否启动 | 必须采用的补救方案 | 程序实现关键差异 |
|---|---|---|---|---|
| 约束含 ≥ 或 ≤,且右端为负(如 x₁ - 2x₂ ≥ -5) | 无法直接构造初始可行基 | ❌ 启动即失败 | 必须引入人工变量,再分大M或两阶段 | 大M法:目标函数叠加 M·aᵢ;两阶段:第一阶段最小化 Σaᵢ |
| 原始问题本身无可行解(如 x₁ + x₂ ≤ 1 且 x₁ + x₂ ≥ 3) | 可行域为空集 | ✅ 能启动,但终表人工变量 > 0 | 两阶段法第一阶段目标值 > 0 → 直接报错;大M法终表含未消去M项 → 判定无解 | 两阶段需独立运行两次单纯形;大M法一次完成但需解析含M表达式 |
| 存在退化(某基变量取值为0),导致迭代循环或人工变量滞留 | 基矩阵奇异,出基规则失效 | ⚠️ 可能卡死或收敛极慢 | 两阶段法更鲁棒:第一阶段目标函数严格递减,避免退化循环;大M法需额外防循环策略 | 两阶段法第一阶段无需处理原目标函数,计算量更小 |
提示:SciPy 的
linprog默认使用修正单纯形法(revised simplex)或内点法,不显式引入人工变量。它通过预处理自动检测不可行性,并在无解时返回status=2。但当你需要解释“为什么无解”(比如告诉业务方“第3条产能约束与第7条交货期约束冲突”),就必须用大M或两阶段法——因为人工变量的最终取值直接对应具体约束的违反程度。
2.2 大M法:用惩罚项把人工变量“逼出”基,但M不是越大越好
大M法的核心思想是:给每个引入的人工变量 aᵢ 在目标函数中添加一个极大的惩罚项 M·aᵢ(max问题加 -M·aᵢ,min问题加 +M·aᵢ),迫使单纯形迭代过程中尽可能让 aᵢ = 0。
但M的取值是玄学陷阱。常见错误是设 M = 1e6,结果数值不稳定;或设 M = 100,却因原目标系数本身达 1e4 而失效。正确做法是:
M 必须大于原问题目标函数系数绝对值之和的 10 倍以上。
例如原问题 min z = 3x₁ + 5x₂,约束引入2个人工变量,则 M > 10 × (|3| + |5|) = 80,实践中取 M = 1000 更安全。
程序实现时,关键不是“加M”,而是如何符号化处理含M的代数式。不能直接用浮点数存储 M,否则迭代中 M 项会被截断。必须将单纯形表中的每个元素表示为c0 + c1*M的二元组,其中 c0 是常数项,c1 是M的系数。只有当 c1 = 0 时,才按常规比较大小;否则优先比较 c1(因M极大)。
下面是一个最小化问题的大M法初始化示例(含符号化处理):
# 假设原问题:min z = 2x1 + 3x2 # 约束:x1 + x2 >= 4 → x1 + x2 - s1 + a1 = 4 (s1为剩余变量,a1为人工变量) # x1 - x2 <= 2 → x1 - x2 + s2 = 2 (s2为松弛变量) # x1, x2, s1, s2, a1 >= 0 # 初始基变量:a1, s2 # 大M目标:min z' = 2x1 + 3x2 + M*a1 # 将a1用其他变量表示:a1 = 4 - x1 - x2 + s1 → 代入z'得: # z' = 2x1 + 3x2 + M*(4 - x1 - x2 + s1) = (2-M)x1 + (3-M)x2 + M*s1 + 4M # 因此初始单纯形表的目标行(检验数行)为: # [2-M, 3-M, 0, M, 0, 4M] ← 注意:s1是松弛变量,系数为0;a1是基变量,不出现在非基列 # 但程序中不能存浮点数4M,而应存元组 (4, 1) 表示 4 + 1*M逻辑说明:这里2-M实际存储为(-1, 1)(即 -1 + 1×M),因为 M 极大,所以该检验数恒为负,x1 必然进基。所有含M的运算必须保持符号化,否则迭代三轮后M项消失,你就失去了判定依据。
参数说明:
M_value:仅用于最终输出时数值化展示,绝不参与迭代计算;symbolic_coeff:二维列表,每行是[c0, c1],代表c0 + c1*M;- 检验数比较规则:先比
c1,c1相同时再比c0; - 出基规则中比值
b_i / a_ij若a_ij含M项(即c1 != 0),则该比值视为无穷大,跳过此行。
3. 两阶段法:用“可行性探针”代替惩罚项,程序结构更清晰但需两次迭代
3.1 第一阶段:构造辅助问题,只关心“能不能活下来”
两阶段法抛弃了M的数值难题,转而构造一个全新的辅助问题(Phase I):
- 目标:最小化所有人工变量之和,即 min w = Σ aᵢ
- 约束:与原问题完全相同(包括所有人工变量)
- 意义:w 的最小值就是原问题可行域到原点的“距离”。若 w* = 0,说明存在一组 x 使所有 aᵢ = 0,即原问题可行;若 w* > 0,则无可行解。
程序上,第一阶段与标准单纯形法完全一致,只是目标函数系数全为 0(非人工变量)或 1(人工变量)。其优势在于:
- 无M值设定风险;
- 迭代过程全是整数/浮点运算,数值稳定;
- 终表 w* 值直接给出可行性结论,无需解析符号表达式。
关键实现细节:第一阶段结束后,需清理人工变量列。若 w* = 0,但基中仍含人工变量(退化情形),必须将其换出——因为人工变量虽取值为0,但占据基位置会干扰第二阶段目标函数系数计算。
# 第一阶段结束后的基变量检查(伪代码) def phase1_cleanup(basic_vars, tableau, art_vars): """ basic_vars: 当前基变量索引列表,如 [2, 5] 表示第2、第5列为基 tableau: 当前单纯形表(不含人工变量列) art_vars: 人工变量原始索引列表,如 [3, 4] 表示第3、4列是人工变量 """ # 找出基中仍存在的人工变量列索引 art_in_basis = [i for i in basic_vars if i in art_vars] for art_col in art_in_basis: # 在art_col所在行,找一个非人工、非基的列j,其系数非零 row = get_row_of_basic_var(tableau, art_col) # 获取art_col对应的行 pivot_col = find_nonzero_nonart_col(row, art_vars, basic_vars) if pivot_col is not None: # 以(row, pivot_col)为轴心进行旋转变换 pivot_operation(tableau, row, pivot_col) # 更新basic_vars:用pivot_col替换art_col basic_vars[basic_vars.index(art_col)] = pivot_col else: # 该人工变量所在行全为0(除右端项),可直接删除整行和该列 delete_row_and_col(tableau, row, art_col) basic_vars.remove(art_col)逻辑说明:这段代码解决的是“人工变量取0却赖在基里”的退化问题。它强制将人工变量换出,确保第二阶段开始时基变量全为原变量或松弛/剩余变量。若找不到非零非人工列,则说明该约束是冗余的,可安全删除——这是两阶段法自带的模型精简能力。
参数说明:
get_row_of_basic_var():根据基变量索引反查其所在行号,需维护行-基变量映射表;find_nonzero_nonart_col():遍历该行所有非基列,跳过人工变量列,找到第一个非零系数;delete_row_and_col():物理删除矩阵行与列,需同步更新所有索引映射。
3.2 第二阶段:无缝切换目标函数,但要注意系数重算
第一阶段成功(w* = 0)后,第二阶段并非简单地“换目标函数继续算”。必须:
- 删除所有人工作变量列(包括它们在约束中的系数);
- 用原始目标函数重算当前基下的检验数;
- 重新计算右端项 b_i(因删除列后行变换关系改变)。
最容易被忽略的是第2步。很多初学者直接把第一阶段终表的目标行替换成原目标系数,这是错的——因为基已变,检验数 = c_j - c_B^T B^{-1} A_j,必须用当前基矩阵 B 重新计算。
# 第二阶段初始化:用当前基重算检验数(以min问题为例) def recalc_phase2_objective(tableau, basic_vars, c_original): """ tableau: 第一阶段终表(含人工变量列) basic_vars: 当前基变量索引(已清理人工变量) c_original: 原问题目标函数系数向量,长度=原变量数+松弛/剩余变量数 """ # 步骤1:提取当前基矩阵B(从tableau中取basic_vars对应列) B = extract_submatrix(tableau, col_indices=basic_vars) # 步骤2:计算c_B(基变量对应的目标系数) c_B = [c_original[i] for i in basic_vars] # 注意:c_original索引需与tableau列对齐 # 步骤3:计算B的逆(用numpy.linalg.inv,或更稳定的LU分解) B_inv = np.linalg.inv(B) # 步骤4:对每个非基变量j,计算检验数 sigma_j = c_j - c_B^T @ B_inv @ A_j n_cols = tableau.shape[1] - 1 # 减1是去掉右端列 sigma = np.zeros(n_cols) for j in range(n_cols): if j in basic_vars: sigma[j] = 0 # 基变量检验数恒为0 else: A_j = tableau[:, j] # 第j列(不含右端项) sigma[j] = c_original[j] - np.dot(c_B, B_inv @ A_j) return sigma逻辑说明:这段代码是第二阶段正确启动的基石。它不依赖第一阶段的目标行,而是从头计算检验数。若跳过此步,第二阶段可能立即选错进基变量,导致迭代发散。实践中,我一般会在 Phase I 结束后打印c_B,B_inv和首行sigma,肉眼核对前几项是否符合预期(如松弛变量检验数应为原目标系数)。
参数说明:
extract_submatrix():需支持从增广矩阵中按列索引提取子矩阵;c_original长度必须与tableau列数(不含右端)一致,缺失的人工变量位置填0;np.linalg.inv()在B接近奇异时会报警,此时应触发退化处理(如Bland规则)。
4. 避坑:大M法和两阶段法程序实现的5个血泪经验
4.1 现象:大M法终表中检验数全≥0,但人工变量a₁=0.0001,目标值含M项
原因:M值不够大,导致 -M·a₁ 的惩罚不足以压倒原目标函数项;或浮点精度丢失,符号化计算中c1被截断为0。
解决:① 重新计算所需M下限:M > max(|c_j|) × n_var × 10;② 强制所有含M运算使用fractions.Fraction或自定义符号类,禁用float;③ 终表检查时,对每个人工变量列,验证其所在行的c1是否为0——若非0,直接判定“M不足”。
4.2 现象:两阶段法Phase I终表w*=0,但第二阶段迭代两轮后目标值突变为nan
原因:Phase I清理人工变量时,删除了某行后未同步更新右端项b_i,导致B矩阵维度与b向量不匹配;或c_B向量索引错位(把人工变量系数当成了原变量)。
解决:① 每次删除行列后,用assert tableau.shape[0] == len(basic_vars)校验;②c_B必须严格按basic_vars中的索引从c_original取值,禁止用“第i个基变量对应c_original[i]”这种想当然逻辑;③ 第二阶段首步必做:用np.allclose(B @ x_B, b)验证当前基解是否满足约束。
4.3 现象:约束含等式(=)时,大M法引入两个人工变量a₁,a₂,终表a₁+a₂=5>0,但报告“无可行解”
原因:等式约束 x₁ + x₂ = 4 应只引入一个人工变量(a₁),写成 x₁ + x₂ + a₁ = 4;引入两个是概念错误,导致惩罚项重复计算。
解决:预处理约束时,对每个等式约束,只加1个人工变量;对≥约束,减剩余变量s再加a;对≤约束,加松弛变量s即可,不加人工变量。程序中需用正则表达式识别约束类型:r'<=|>=|='。
4.4 现象:输入约束右端为负(如 x₁ - x₂ ≥ -3),程序直接报“b_i < 0”,拒绝运行
原因:未做约束标准化。≥负数约束应先两边乘-1转化为 ≤ 正数,再加松弛变量;或直接写为 -x₁ + x₂ ≤ 3。
解决:在读入约束后、构建单纯形表前,强制执行:
if b_i < 0 and constraint_type == '>=': A_i *= -1 b_i *= -1 constraint_type = '<='此步必须在人工变量引入之前完成,否则会多引入不必要的aᵢ。
4.5 现象:两阶段法Phase I迭代100轮不收敛,目标w持续微降(如1e-10→1e-11)
原因:退化导致循环(cycling)。标准Bland规则(选最小下标进/出基)在此场景下失效,因人工变量索引通常较大。
解决:① 启用扰动法(perturbation):给右端项b_i加极小随机扰动(1e-12);② 改用字典序规则(lexicographic rule):比较比值时,不仅比b_i/a_ij,当相等时按行索引字典序选最小者;③ 实际项目中,我直接限定最大迭代次数为3*n_var,超时则切换至内点法备用路径。
5. 验证与调试:用三组黄金测试用例守住程序正确性的底线
5.1 测试用例设计原则:覆盖所有失效模式
不能只用教科书例题(如“木匠做桌子椅子”)验证程序。必须构造三类黄金用例,每类直击一种核心风险:
| 类型 | 目标 | 关键特征 | 预期输出 |
|---|---|---|---|
| 可行性边界用例 | 检验无解判定能力 | 约束明显矛盾:x₁+x₂≤1 且 x₁+x₂≥2 | 大M法:终表含未消M项;两阶段法:Phase I w*>0 |
| 退化陷阱用例 | 检验循环规避能力 | 多个约束交于一点,如 x₁≥0, x₂≥0, x₁+x₂≥0, x₁+2x₂≥0 | 两阶段法Phase I应在5轮内收敛;大M法需触发Bland规则 |
| M敏感度用例 | 检验符号化计算可靠性 | 原目标系数达1e5,M设为1e6时失败,M=1e7时成功 | 改变M值,程序应稳定输出相同解,仅数值精度变化 |
下面给出可行性边界用例的完整数据与验证脚本:
# 黄金测试1:无可行解(经典矛盾约束) test_case_infeasible = { "objective": "min", "c": [1, 1], # min z = x1 + x2 "A": [ [1, 1], # x1 + x2 <= 1 [1, 1] # x1 + x2 >= 2 → 标准化为 -x1 -x2 <= -2 ], "b": [1, -2], "sense": ["<=", ">="] } # 预处理后实际约束(注意第二行已乘-1): # x1 + x2 <= 1 → x1 + x2 + s1 = 1 # -x1 -x2 <= -2 → -x1 -x2 + s2 = -2 → 乘-1得 x1 + x2 - s2 = 2 → 加人工变量a1: x1 + x2 - s2 + a1 = 2 # 故需1个人工变量a1 # 验证脚本 def test_infeasible(): solver = SimplexSolver() result = solver.solve(test_case_infeasible) # 大M法断言 assert result["method"] == "big_m" assert result["status"] == "infeasible" assert abs(result["objective_value"]) > 1e6 # 含M项主导 # 两阶段法断言 solver2 = TwoPhaseSolver() result2 = solver2.solve(test_case_infeasible) assert result2["phase1_objective"] > 0.1 # w* 显著大于0 assert result2["status"] == "infeasible" test_infeasible()逻辑说明:这个测试不验证“解是什么”,而验证“程序是否正确喊出‘无解’”。result["status"]必须为"infeasible",且大M法的objective_value绝对值应远超原目标量级(证明M项未被忽略),两阶段法的phase1_objective必须 > 0。任何一项失败,说明人工变量判定逻辑有漏洞。
参数说明:
sense字段必须显式传入["<=", ">="],程序据此决定是否引入人工变量;b = [-2]触发预处理中的符号翻转,是检验标准化模块的关键;- 断言
abs(objective_value) > 1e6是数值化的M存在性证明,比检查字符串更可靠。
5.2 调试技巧:把单纯形表变成“可读日志”,而非黑匣子
当程序输出异常时,90%的问题藏在中间表中。我坚持三个调试习惯:
每轮迭代打印压缩表:只显示基变量、检验数、右端项,格式对齐:
Iter 0: Basis=[a1,s2] Sigma=[2-M,3-M,0,M,0] b=[4,2] Iter 1: Basis=[x1,s2] Sigma=[0,1-M,-2+M,2,0] b=[4,-2] ← 发现b[1]<0!立刻停机检查约束标准化对含M项做颜色标记:终端输出中,
c1!=0的数字用红色,c1==0用绿色。一眼看出哪些行还受M控制。导出CSV供Excel分析:将每轮
tableau保存为CSV,用Excel条件格式高亮负检验数、负右端项、含M列。曾靠此发现某次迭代中a_ij计算符号错误(本该为负却得正),Excel排序后立即暴露。
最后说个血泪教训:某次交付前夜,客户临时增加一个含12个变量的约束组,程序在Phase I卡死。我按上述调试法导出第50轮表,发现某行右端项为1.23e-17——这是浮点误差累积的退化信号。立刻启用扰动法,加1e-15后,5轮解决。不要迷信“理论上收敛”,工程中必须给算法加现实世界的缓冲垫。
希望帮到你。
本文还有配套的精品资源,点击获取