news 2026/9/10 5:33:57

MATLAB实现二阶段单纯形法:原理、代码与排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MATLAB实现二阶段单纯形法:原理、代码与排错

简介:二阶段法与单纯形法的MATLAB实现代码包,专为学习线性规划算法的学生、科研人员及工程师设计,重点解决初始解不可行情况下如何求出可行解并进一步寻优的问题。代码包共3个文件,主程序以.m脚本实现完整的两阶段法流程,配套.asv文件为编辑过程中生成的自动备份,另有.txt说明文档帮助理解算法步骤与函数调用方式。整个包仅8KB,结构精炼,适合直接阅读和调试。内容涉及如何定义线性规划目标函数与约束、如何通过第一阶段构造人工变量得到初始可行解、如何转入第二阶段调用单纯形法迭代求最优值,并涵盖无界解、无解等异常情况的处理思路。目前已有374人学习浏览,对于希望借助MATLAB快速掌握两阶段法和单纯形法原理的读者而言,这是一份可直接运行、便于对照算法流程的入门与进阶参考资料。

1. 二阶段法matlab:单纯形表里没有单位列时,才是真正入口

手写单纯形法时,教科书默认你手里有一张现成的单纯形表——单位列排好,初始可行基直接可见。换成 matlab 自己搭模型就会发现,"≤" 约束补出的松弛变量确实带来单位列,"≥" 约束补出的剩余变量系数是 −1,等式约束什么也补不出来,表的左侧根本没有单位矩阵。二阶段法就是为缺基问题设计的标准解法:第一阶段用人工变量和辅助目标强行制造可行基,第二阶段卸掉人工变量、恢复原目标继续迭代。下面按原理、实现、参数、排错四个层次展开,适合运筹学课程、算法项目以及想绕开 linprog 黑盒的工程师。

2. 二阶段单纯形法原理:人工变量、辅助目标与可行基判定

2.1 为什么没有初始可行基:松弛变量只解决一半问题

单纯形法的每次迭代都要求当前基是可行基,也就是基变量取值全部非负。模型若全是 "≤" 约束,加松弛变量后天然得到一个单位矩阵,初始可行基零成本获得。实际建模里 "≥" 约束和等式约束出现频率很高:运输问题里的下限约束、配方问题的比例约束、逻辑约束转换出来的等式,都会让系数矩阵缺列。

以这个最小化问题为例:

min z = x1 + x2 s.t. 2x1 + x2 ≥ 4 x1 + 3x2 ≥ 6 x1, x2 ≥ 0

补剩余变量 x3、x4 后得到标准形:

2x1 + x2 − x3 = 4 x1 + 3x2 − x4 = 6

x3、x4 的系数是 −1,不是 +1 的单位列,两行都没有可用的初始基。单纯形法要求初始基可行,这一步卡住,后续的转轴公式就全部无从谈起。matlab 优化工具箱里的 linprog 默认对偶单纯形能自己处理这种情况,但手写实现时,两阶段法是最直接、数值上也最干净的选择。

2.2 第一阶段:把目标函数换成人工变量之和

给缺基的每一行补一个人工变量,系数取 +1,构造辅助问题:

min w = x5 + x6 2x1 + x2 − x3 + x5 = 4 x1 + 3x2 − x4 + x6 = 6 x1..x6 ≥ 0

基变量取 x5、x6,初始可行基立即可得。第一阶段的目标只有一个判定作用:原问题有可行解,当且仅当最优的 w = 0。w 是人工变量之和,天然非负;w = 0 说明所有人工变量恰好取 0,剩下的 x1..x4 满足原约束,人工列只是第一阶段迭代用的脚手架。

回到例子。初始解 x5 = 4、x6 = 6,w = 10。把 w 写成非基变量的表达式:

w = 10 − 3x1 − 4x2 + x3 + x4

min 问题取检验数最负的变量进基,x2 的检验数 −4 最负,进基。最小比值 min(4/1, 6/3) = 2,x6 出基。转轴后 w = 2 − (5/3)x1 + x3 − (1/3)x4 + (4/3)x6,x1 的检验数 −5/3 最负,进基;最小比值 min(2/(5/3), 6/(1/3)) = 6/5,x5 出基。此时 x1 = 6/5、x2 = 8/5,w = 0,第一阶段结束。

迭代进基变量出基变量最小比值说明w 值
1x2(检验数 −4)x6min(4/1, 6/3) = 22
2x1(检验数 −5/3)x5min(2/(5/3), 6/(1/3)) = 6/50

若最优 w > 0,说明至少一个人工变量必须取正值才能满足约束,原问题无可行解,直接返回。这也是二阶段法和大 M 法的主要差别:大 M 法把人工变量乘一个很大的 M 塞进原目标,M 选取不当会让数值问题病态化;二阶段法把可行性判定拆成独立的第一阶段,不引入额外大数,数值行为更可预期。

2.3 第二阶段:丢弃人工列,恢复原目标继续迭代

上例第一阶段结束时基是 {x1, x2},没有人工变量残留。删掉人工列,目标换回 z = x1 + x2,用同一个基继续迭代。解 B'λ = cB 得到 λ = (0.4, 0.2),非基变量检验数 r3 = 0.4、r4 = 0.2 全部非负,已经满足 min 问题的最优性判定,不需要再转轴。

这里有个容易被忽略的细节:第一阶段的最优基里可能残留值为 0 的人工变量,原因通常是退化或冗余约束。此时不能直接删人工列,否则基矩阵少一列。标准做法是找该行一个非零且不在基内的原变量列做一次退化转轴,把人工变量换出;如果整行系数全是 0,说明该约束是其他行的线性组合,整行删掉。这个逻辑在下一章的代码里单独占一个步骤,也是两阶段法手写实现里报错率最高的地方。

注意:第二阶段换回原目标后,检验数必须按新目标从头重算。沿用第一阶段残留的检验数是最常见的错误,因为对偶变量 λ 依赖基变量的目标系数 cB,目标一变,全部判别数失效。

3. matlab两阶段法完整实现:simplex_core 与人工变量清基

3.1 输入约定:标准形、b ≥ 0 与不等式转换

代码按标准形 min c'x、Ax = b、x ≥ 0 书写。调用前需要自己完成两步:不等式补松弛变量或剩余变量,再整理成等式;右端项正负由函数内部处理。函数对 b < 0 的行自动翻号,调用方少一个出错点。翻号只改变该行的符号,可行域和解集不变。

3.2 核心迭代:simplex_core

第一阶段和第二阶段共用同一个单纯形核心:给定初始可行基,反复做"算检验数、判定最优、算进基方向、最小比值出基"。检验数用对偶变量计算,不显式求逆:

λ 满足 B'λ = cB,检验数 r = c − A'λ。

function [x, z, basis, iters] = simplex_core(A, b, c, basis, tol) % 单纯形法核心迭代:给定初始可行基,求解 min c'*x % 输入 A(m,n)、b(m,1)、c(n,1)、basis(m,1) 基变量列号、tol 容差 % 输出 x(n,1) 最优解、z 最优值、basis 最终基、iters 实际转轴次数 [m, n] = size(A); iters = 0; while true B = A(:, basis); xB = B \ b; % 基变量取值,同时是可行性判定依据 lambda = B' \ c(basis); % 解 B'*lambda = cB,得到对偶变量 r = c - A' * lambda; % 所有列的检验数 reduced cost r(basis) = 0; % 数值上强制基变量检验数为 0 [rmin, q] = min(r); if rmin >= -tol % min 问题:检验数全非负即最优 x = zeros(n, 1); x(basis) = xB; z = c' * x; return; end d = B \ A(:, q); % 进基方向 if max(d) <= tol error('two_phase:unbounded', '检验数为负但进基方向无正分量,问题无界'); end ratio = xB ./ d; % 最小比值法则 ratio(d <= tol) = Inf; % 非正方向不参与比值 [~, p] = min(ratio); % p 行出基,q 列进基 basis(p) = q; iters = iters + 1; if iters > 10000 error('two_phase:maxiter', '迭代超过 10000 次,疑似退化循环'); end end end

几个关键点说明:

  • B \ bB' \ c(basis)走的是 MATLAB 的 LU 分解求解路径,不要写成inv(B) * b。显式求逆在维数超过几十、条件数偏大的时候,误差会直接污染后续转轴。
  • r(basis) = 0是必要的数值修正。浮点运算下基变量检验数会残留 1e-16 量级的噪声,不置零可能把最优基误判为仍有负检验数,导致多余的转轴甚至循环。
  • 无界判定依据是:存在负检验数,但该方向 d 的分量全部小于等于 0,增大进基变量不会碰到任何约束上界。这里用max(d) <= tol而不是all(d <= 0),容差同样施加在这一步。

3.3 主函数:两阶段衔接与人工变量清基

主函数把"补人工变量、第一阶段、清基、第二阶段"串起来。第一阶段的目标向量只在人工变量列位置置 1,其余位置为 0。

function [x, z, info] = two_phase_simplex(A, b, c, tol) % 二阶段法 matlab 实现:min c'*x,s.t. A*x = b,x >= 0 % 输入 A 等式约束系数 m*n,b 右端项 m*1(负分量自动翻号),c 目标系数 n*1 % 输出 x 最优解(无可行解时为空),z 最优值(不可行时为 Inf) % info.status = 'optimal' / 'infeasible' % info.phase1_iters / info.phase2_iters 记录两阶段转轴次数 if nargin < 4, tol = 1e-9; end [m, n] = size(A); info = struct('phase1_iters', 0, 'phase2_iters', 0, 'status', 'optimal'); % ---- 1. b 负行翻号 ---- neg = find(b < 0); A(neg, :) = -A(neg, :); b(neg) = -b(neg); % ---- 2. 找天然单位列 ---- basis = zeros(m, 1); for i = 1:m for j = 1:n if abs(A(i,j) - 1) < tol && ... abs(sum(abs(A(:,j))) - 1) < tol && ... ~ismember(j, basis) basis(i) = j; break; end end end % ---- 3. 给缺基行补人工变量,构造第一阶段矩阵 ---- artRows = find(basis == 0); k = numel(artRows); A1 = [A, zeros(m, k)]; for t = 1:k A1(artRows(t), n + t) = 1; basis(artRows(t)) = n + t; end c1 = [zeros(n, 1); ones(k, 1)]; % 第一阶段目标:人工变量之和 % ---- 4. 第一阶段迭代 ---- [~, w, basis1, it1] = simplex_core(A1, b, c1, basis, tol); info.phase1_iters = it1; if w > tol % 辅助目标没压到 0 => 原问题不可行 info.status = 'infeasible'; x = []; z = Inf; return; end % ---- 5. 清掉基里残留的人工变量(值为 0 的退化情形) ---- artCols = n + (1:k); delRows = []; for t = find(ismember(basis1, artCols))' % 找该行非零、且不在基内的原变量列 cand = find(abs(A1(t, 1:n)) > tol & ~ismember(1:n, basis1)); if isempty(cand) delRows = [delRows, t]; % 该行线性相关,冗余约束,整行删掉 else basis1(t) = cand(1); % 退化转轴:人工变量出基 end end if ~isempty(delRows) A1(delRows, :) = []; b(delRows) = []; basis1(delRows) = []; end % ---- 6. 第二阶段:丢人工列,换回原目标 ---- A2 = A1(:, 1:n); [x, z, ~, it2] = simplex_core(A2, b, c, basis1, tol); info.phase2_iters = it2; end

3.4 三个容易写错的细节

第一,第 2 步用sum(abs(A(:,j))) ≈ 1判单位列,等价于"该列只有一个非零元素且模为 1",配合A(i,j) ≈ 1就锁定了那一个非零元素所在的行。比逐行索引做差集更简洁,也避免 m = 1 时索引向量为空导致&&运算报错。

第二,第 5 步必须先收集delRows再统一删除,不能边遍历边删行。边删边遍历会让后续行号错位,这是手写两阶段法最常见的隐性 bug,症状是"第一次运行正常,第二次结果完全不对"。

第三,第 5 步对 tol 的依赖方式和第 2 步不一样:第 2 步要求列近似单位列,第 5 步只要求系数非零。如果把 tol 调大,第 5 步可能把微小非零系数当成零,误判冗余约束而删行——这个错误比第 2 步的误判隐蔽得多。所以默认 tol 维持 1e-9 不要轻易动。

4. 二阶段法求解算例与参数调整:tol、迭代上限与 Bland 法则

4.1 跑通最小算例,核对输出

把两个函数存成 m 文件,运行测试脚本:

% 例:min z = x1 + x2 % 约束 2x1 + x2 >= 4;x1 + 3x2 >= 6;x1,x2 >= 0 % 标准形:补剩余变量 x3、x4,等价 form 见正文 2.1 节 A = [2 1 -1 0; 1 3 0 -1]; b = [4; 6]; c = [1; 1; 0; 0]; [x, z, info] = two_phase_simplex(A, b, c); fprintf('x = [%g %g %g %g]\n', x); fprintf('z = %g\n', z); fprintf('phase1 pivots = %d, phase2 pivots = %d\n', ... info.phase1_iters, info.phase2_iters);

期望输出:x = [1.2 1.6 0 0],z = 2.8,phase1 pivots = 2,phase2 pivots = 0。第二阶段零转轴是正常现象:第一阶段结束时的基有时恰好让原目标的检验数全部非负。但绝不能因此省略第二阶段——换目标后检验数必须从头重算,这一条是两阶段法正确性的底线。验证时重点看 w 是否为 0、x 是否满足 A*x = b,这两点比目标值更先暴露问题。

4.2 三个必调参数:容差、迭代上限、Bland 法则

手写单纯形法要调的参数不多,但每个都能让结果从"对"变成"错"。

参数代码位置作用建议值
toltwo_phase_simplex 第 4 个入参检验数、最小比值、可行性判定共用1e-9
迭代上限simplex_core 底部iters > 10000防止退化循环死转10000
Bland 开关进基/出基选择处手动替换消除退化循环出现循环时开启

tol 太大(如 1e-6)会吃掉真实的负检验数,输出一个接近最优但不是最优的解;tol 太小(如 1e-12)会把浮点噪声当成合法转轴方向,在最优基附近反复横跳。1e-9 对绝大多数双精度算例够用,碰到量级差异极大的矩阵,先做行缩放而不是改容差。

退化循环的现场特征是:某次转轴后目标值不再下降,但检验数里仍有负值。Bland 法则(最小下标规则)是教科书上保证终止的进基/出基规则——在负检验数里选下标最小的进基,在最小比值并列的行里选基变量列号最小的出基。替换 simplex_core 中的两处选择:

% 替换进基选择:负检验数里取最小下标 negIdx = find(r < -tol); q = negIdx(1); % 替换出基选择:比值并列时取基变量列号最小 [minVal, ~] = min(ratio); ties = find(abs(ratio - minVal) < tol); [~, idx] = min(basis(ties)); p = ties(idx);

注意 Bland 法则在浮点实现里必须基于 tol 过滤后的负检验数列表,否则 1e-16 的负噪声会破坏"最小下标"的语义。代码里保留 10000 次上限,是因为很多退化实例靠随机抖动能自己脱离,上限只当保险丝用。

4.3 随机算例交叉验证:与 linprog 逐个比对

验证两阶段法实现没有写错,最可靠的办法是随机生成一批已知可行的问题,和 linprog 对比最优值:

rng(42); bad = 0; for t = 1:50 m = 4; n = 8; A = randn(m, n); x0 = rand(n, 1) + 0.1; % 随机正解,保证原问题可行 b = A * x0; c = rand(n, 1) + 0.5; % 目标系数取正,配合 x>=0 保证有下界 [x1, z1, info] = two_phase_simplex(A, b, c); opt = optimoptions('linprog', 'Display', 'off'); [~, z2] = linprog(c, [], [], A, b, zeros(n, 1), [], [], opt); if ~strcmp(info.status, 'optimal') || abs(z1 - z2) > 1e-6 bad = bad + 1; fprintf('第 %d 组不一致:self=%g linprog=%g\n', t, z1, z2); end end fprintf('不一致实例数:%d / 50\n', bad);

这段验证的要点:b 用 A*x0 生成,保证可行域非空,第一阶段必然收敛到 w = 0;约束全是等式,正好覆盖二阶段法最需要人工变量的场景。目标系数取正,配合 x ≥ 0 保证最优值有下界,避免随机出无界实例干扰比对。若出现不一致,先打印该组的 A、b、c 手工检查,多数问题出在容差设置而不是算法逻辑。

提示:simplex_core 返回的 iters 只统计实际转轴次数,最后一次纯最优性校验不计数。调试时如果 phase2_iters 为 0,不代表第二阶段没执行,只是没有发生转轴。

5. 二阶段法排错:无界、不可行与人工变量残留的定位

5.1 三类报错的快速定位

无界错误来自 simplex_core 抛出的two_phase:unbounded。先查模型抄写:等式约束的符号、决策变量非负限制有没有遗漏。无界多半是少写了一个约束,而不是代码问题。第二阶段报无界而第一阶段正常,重点检查原目标 c 的符号是否写反。

不可行状态来自第一阶段 w > tol。先把 tol 调到 1e-10 重跑一次,排除容差误判;仍然不可行再用 linprog 跑同一模型复核。矛盾约束如 x1 ≥ 5 和 x1 ≤ 3 同时出现是最常见的来源,这类问题第一阶段结束时的 w 会明显大于 0,不是贴边的那种。

5.2 收敛断言放在 return 之前

在 simplex_core 的 return 前加两行断言,把"解不可行"和"目标值不对"两类错误分开:

resid = norm(A * x - b) / (1 + norm(b)); assert(resid < 1e-8, '可行性断言失败:残差 %g', resid); assert(all(x > -1e-9), '可行性断言失败:存在负分量');

残差过大说明基变量取值和约束对不上,问题出在转轴过程;只有负分量说明解的符号出了问题,多半是目标方向或翻号逻辑写反。这两条断言在任何规模的算例上都适用,是比对照最优值更前置的检查。

5.3 规模变大时的两个改动

数据规模上去之后,两个位置最先遇到瓶颈。一是simplex_core里每次转轴都调用三次B \ bB' \ c(basis)B \ A(:,q),应该改为在一次[L,U] = lu(B)后复用分解结果,每次转轴只做回代。二是现在维护的是整张表格式单纯形,稀疏大规模问题应当改成修正单纯形(revised simplex),只存基矩阵,检验数按列逐个计算而不是更新整张表。两阶段法的结构不变,变的只是线性方程组的求解方式和检验数的计算粒度。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 5:31:25

GD32启用FPU与CMSIS-DSP实战:避免HardFault的完整配置链路

简介&#xff1a;本资源面向GD32嵌入式开发工程师及进阶学习者&#xff0c;聚焦浮点运算与数字信号处理能力提升&#xff0c;系统解决FPU启用、CMSIS-DSP库集成及高性能算法落地等核心问题。资源包共3个文件&#xff0c;含1个预编译浮点数学库&#xff08;arm_cortexM4lf_math.…

作者头像 李华
网站建设 2026/9/10 5:30:40

DS3502快速写入模式在MicroPython波形生成中的实战应用

1. 项目概述&#xff1a;为什么 DS3502 在 MicroPython 嵌入式场景里值得深挖&#xff1f;MicroPython 在资源受限的嵌入式设备上跑得稳、写得快、调试方便&#xff0c;但很多人卡在“能点亮 LED”和“真能干活”之间——尤其是需要精确时序、高频响应或模拟信号生成的场景。这…

作者头像 李华
网站建设 2026/9/10 5:30:33

Rust嵌入式开发入门:microduck最小可行范式与ESP32-C3实战

1. 什么是microduck&#xff1f;它不是玩具&#xff0c;而是一套嵌入式系统开发的“最小可行范式” microduck这个词&#xff0c;最近半年在Rust嵌入式圈子里突然高频出现&#xff0c;但它 不是某个厂商注册的硬件型号&#xff0c;也不是开源社区官方命名的标准项目 。我第一…

作者头像 李华
网站建设 2026/9/10 5:29:58

ESP32-S3端云协同AI架构:轻量级边缘智能落地实践

1. 项目概述&#xff1a;为什么一块 ESP32-S3 能成为 AI 陪伴设备的起点&#xff1f;你手头那块不到三十块钱的 ESP32-S3 开发板&#xff0c;真能跑 AI&#xff1f;不是演示 Demo&#xff0c;不是调个 API 就完事&#xff0c;而是实打实听懂你说话、记住你习惯、在本地做决策、…

作者头像 李华
网站建设 2026/9/10 5:29:46

C语言结构体完全指南:从语法到内存对齐的工程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华