☰
线性规划求解器实现:单纯形法、大M法与两阶段法完整指南
2026/10/23 13:18:46 网站建设 项目流程

简介:这份资源面向学习最优化方法、线性规划课程的高校学生与工程技术人员,聚焦单纯形法求解中的大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_ub=A, b_ub=b, 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', encoding='utf-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法,都强制先跑一遍两阶段法做对照,两个结果一致才敢用。希望这个习惯也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询