1. 线性规划到底在解决什么问题
1.1 先理解目标函数与约束条件的真实含义
很多刚接触优化这个方向的人,碰到“线性规划”四个字第一反应是“哦,就是一个求最大值或最小值的数学题”。这么理解不算错,但如果你想拿 Matlab 去解实际问题,还必须把这句话往前推一步:线性规划处理的是“有限资源下怎么调配才最划算”的那类决策问题。
举个例子,你有两台机器,机器A每天最多运转8小时,机器B每天最多运转6小时,两种产品都要用到这两台机器。产品1每件利润50块,产品2每件利润80块。这时候问题来了:各做多少件,才能让一天的总利润最高?这就是典型的线性规划问题。变量是两种产品的产量,追求的是“利润最大”,约束条件是“机器时长不能超”和“产量不能为负”。
数学模型写出来长这样:
max z = 50*x1 + 80*x2 s.t. 3*x1 + 4*x2 <= 8 2*x1 + 5*x2 <= 6 x1 >= 0 x2 >= 0那为什么一定要强调“线性”?因为这个假设决定了整个问题的难度和算法选择。目标函数和约束条件里每个变量都是“一次幂”,没有 x1^2、没有 x1*x2、没有 sin(x1),整个问题就是一组直线、平面围出来的区域。在这个区域里找最优点,数学家证明过,最优解一定出现在区域的顶点上。这句话是线性规划的灵魂:你不用在整个区域里翻来翻去找,只需要沿着顶点一个个试探就行了。
用 Matlab 做这件事,最关键的本事不是会写linprog函数,而是把一个现实问题干净利落地翻译成一组线性式子。很多人直接卡在建模这一步:约束条件写错方向、变量含义不清晰、目标函数和目标方向没想明白。后面我会专门讲这些坑,这里先记住一句结论——翻译不到位,求解器再强也白搭。
1.2 从标准型到 max、min、不等式、等式——为什么建模是难点
Matlab 的linprog不接受“任意形式”的线性规划,它只认一套标准型:
min f' * x s.t. A*x <= b Aeq*x = beq lb <= x <= ub这套标准型看着简单,但实际项目里你面对的问题千奇百怪,就出现了几个高频转化难点。
第一个难点是目标方向。人的日常思维习惯是“利润最大化”“收益最大化”,但linprog默认是求最小。解决办法很简单:想求最大值,就把目标系数整体取负,改成求min -f'*x,最后把得到的fval再取负回来就是最大值。我在项目里经常看到有人忘了这一步,求解器给出一堆负值,还以为自己写错了。
第二个难点是不等式方向。很多约束是“至少需要”“不得低于”,写成数学就是>=。但标准型里只允许<=。转化也简单,两边同时乘个 -1,方向就翻过来了。比如x1 + 2*x2 >= 10,等价于-x1 - 2*x2 <= -10。
第三个难点是混合约束。既有不等式又有等式,还有上下界。很多人喜欢把所有约束都塞进A和b里,其实没必要。linprog提供了Aeq、beq、lb、ub这几个专门通道,该用哪个就用哪个。尤其是变量的非负限制,直接写在lb里比放进不等式矩阵里清爽得多,去调试的时候一眼就能看出来。
我自己的建模顺序基本固定:先写下变量及其含义,再列目标函数,再逐条列约束,最后统一整理成标准型。中间绝不直接打开 Matlab 写代码,因为脑子里乱成一团时写出来的矩阵必定错。
2. 用 matlab 的 linprog 跑通第一个例子
2.1 linprog 的函数签名与参数配置
linprog的完整用法并不复杂,最常用的是这样:
[x, fval, exitflag, output, lambda] = linprog(f, A, b, Aeq, beq, lb, ub, options)参数从左到右正好对应标准型:目标向量f、不等式系数矩阵A和右端项b、等式系数矩阵Aeq和右端项beq、下界lb、上界ub、求解选项options。
没有的约束位置直接传空数组[],比如你只有不等式约束,就写成:
[x, fval] = linprog(f, A, b, [], [], [], [], [])输出参数里,x是最优解向量,fval是目标函数最优值,exitflag是最重要的状态标志位。它告诉你这次求解到底成没成:1表示收敛到最优解,0表示达到最大迭代次数但仍未收敛,-2表示找不到可行解,-3表示问题无界(目标函数可以无限大或无限小)。调试问题时,我第一眼永远先看exitflag,别的都是后话。
options用optimoptions创建。我在教学和快速验证时最常用的选项是:
options = optimoptions('linprog', 'Algorithm', 'dual-simplex', 'Display', 'iter');Algorithm可选'dual-simplex'、'interior-point'等策略,Display设为'iter'可以实时看到每一轮迭代的日志。对于中小规模问题,双单纯形法通常够快也够稳;内点法在大型问题上通常会更快,但给出的解可能在顶点附近“差一点点”,需要留意。
2.2 一个运输问题的完整建模与求解
纸上谈兵没用,我拿一个典型的运输问题来走完整流程。
假设某个区域有三个仓库,要给四个门店供货。三个仓库的库存分别是 30、50、40 吨,四个门店的需求分别是 25、35、30、30 吨。单位运输成本如下表:
| 仓库\门店 | 门店1 | 门店2 | 门店3 | 门店4 |
|---|---|---|---|---|
| 仓库A | 8 | 6 | 10 | 9 |
| 仓库B | 9 | 12 | 7 | 11 |
| 仓库C | 14 | 7 | 9 | 5 |
问怎么分配运输量,总成本最低。
先建模。设变量x(i,j)表示从第 i 个仓库运到第 j 个门店的吨数,一共有 3 乘 4 等于 12 个变量。目标函数是所有运输量乘以对应成本,再求和。约束分两类:每个仓库运出去的总量不能超过库存,写成等式或不等式看具体问题——库存就是上限,所以用<=;每个门店收到的总量必须满足需求,这里因为是必须保证供应,用等式=。
我习惯把这些变量按“仓库优先”的顺序展开成一维向量:
x = [x11, x12, x13, x14, x21, x22, x23, x24, x31, x32, x33, x34]于是目标向量f就是按这个顺序拉平的 12 个成本系数。
仓库约束是 3 行,每行只管对应仓库的 4 个变量。门店约束是 4 行,每行管涉及该门店的 3 个变量。我自己写的时候从不手拼一个大矩阵,而是用代码动态生成,这样既不容易写错,后面数据改动也方便:
supply = [30; 50; 40]; demand = [25; 35; 30; 30]; cost = [8 6 10 9; 9 12 7 11; 14 7 9 5]; nSupply = length(supply); nDemand = length(demand); nVars = nSupply * nDemand; % 不等式约束:Aineq * x <= bineq Aineq = zeros(nSupply, nVars); for i = 1:nSupply idx = (i-1)*nDemand + (1:nDemand); Aineq(i, idx) = 1; end bineq = supply; % 等式约束:Aeq * x == beq Aeq = zeros(nDemand, nVars); for j = 1:nDemand idx = j:nDemand:nVars; Aeq(j, idx) = 1; end beq = demand; f = cost(:); lb = zeros(nVars, 1); ub = []; [x, fval, exitflag] = linprog(f, Aineq, bineq, Aeq, beq, lb, ub); % 把结果还原成矩阵 x = reshape(x, nDemand, nSupply)'; % 第 i 行第 j 列 = 仓库 i 到门店 j 的量注意这里我用了一个小技巧:等式矩阵生成时用了j:nDemand:nVars这种间隔索引。因为我们的变量是按仓库分组排的,门店 j 在所有仓库组里的位置正好每隔 4 个出现一次。这种向量化写法比在循环里逐个填Aeq(j, idx)=1更简洁,执行速度也快。
跑完后如果exitflag == 1,查看x就能得到一份完整运输方案。我第一次跑这类问题时最大的感受是:建模环节老老实实把矩阵生成逻辑写清楚,比求解本身更花时间,但值得。
3. 求解结果的解读与调试心得
3.1 当求解失败时,如何定位是建模问题还是数值问题
exitflag不是魔鬼数字,它是求解器给你的第一句话。遇到非 1 的结果,人的第一反应往往是“运行出错,赶紧改代码”,但大多数情况下是建模出了问题。
exitflag = -2意味着问题“不可行”,也就是没有任何一组变量取值能满足全部约束。最常见的不可行原因有三个:
第一,供需不匹配。比如仓库总库存只有 80 吨,但四个门店总需求是 120 吨,等式约束永远不可能同时满足。这种数量关系在约束少时肉眼可查,但变量一多就需要自己写一个小检查脚本,先把sum(supply)和sum(demand)比一比。
第二,约束互相矛盾。比如一个变量既被要求x >= 10,又被另一个约束推出x <= 8,那当然无解。调试这种问题,我会把约束一个个删掉,看删掉哪一个后问题就变得可解,那个约束十有八九跟别的约束打架。
第三,上界和下界写反了。lb必须小于等于ub,一旦出现lb(i) > ub(i),求解器直接给你不可行。这个错误很低级,但真实发生的频率不低。
exitflag = -3表示“无界”,常出现在目标函数的符号写反或漏掉了某些关键下界时。我踩过最蠢的一个坑:某变量的lb忘写了,结果求解器发现目标函数可以让这个变量无限增大,直接把fval拉到负无穷(因为我们是在做最小化)。所以给所有负变量建立lb = zeros这个习惯,绝对值多占一行代码,能省几个小时。
至于“数值问题”,这类问题通常表现为exitflag是 0 或者结果看着很奇怪,但建模本身似乎没错。考虑是不是变量量纲差太远——一个变量是 0.001 级别,另一个是 100000 级别,矩阵条件数会变得很糟糕。解决办法也很朴素:缩放变量,让它们的数值范围都落到差不多量级。
3.2 灵敏度分析:让结果成为决策依据
线性规划求解完,很多人的习惯是看一眼x和fval就收工了。但这份结果的价值其实只发挥了一小半。真正能让你在商务讨论或项目汇报里站住脚的,是 answer 后面藏着的影子价格和灵敏度信息。
lambda这个输出就包含着这些信息,它有lambda.ineqlin、lambda.eqlin、lambda.lower、lambda.upper四类。我自己用得最多的,一个是lambda.ineqlin,另一个是lambda.lower。
呼应前面仓库运输的例子:每个仓库的库存约束对应一个影子价格,它的含义是“如果这个仓库的库存多出 1 吨,总运输成本能降低多少”。比如仓库A的影子价格是 -3,那就是说,如果仓库A多放 1 吨货,总成本能再降 3 块。这个数字对业务决策非常有价值:它会告诉你该把预算花在扩容哪个仓库上,而不是平均使劲。
但要注意,影子价格只在约束右端项的一个小邻域内有效。你不可能把库存从 30 吨加到 3000 吨还指望成本按同一个斜率往下降,因为当约束变成松弛状态后,影子价格会跳到 0。我做规划分析时,一定会把采购或扩容的候选方案明确标出“在多大范围内这个边际利好成立”,否则容易被业务方追问到墙角。
用 Matlab 做灵敏度分析时可以顺手对右端项做一个小循环扫描:
for newSupply = 30:2:60 tempB = [newSupply; supply(2); supply(3)]; [~, tempFval] = linprog(f, Aineq, tempB, Aeq, beq, lb, ub); record(end+1) = tempFval; end这样得到一张“库存-总成本”关系表,比只报一个最优值扎实得多。
4. 我在实战中踩过的坑与效率建议
4.1 Degenerate 与 infeasible:别被报错吓到
做线性规划求解器调试,不同的算法面对不可行问题反应不太一样。dual-simplex在检测不可行时常会给出相对明确的信息,而interior-point在某些数值边界上容易卡住不报错也不收敛。所以我有一条经验法则:拿到一个新模型,先用dual-simplex快速验证可行性,再上大规模数据。这相当于先用一张小地图探路,确认路线没有断头路才上高速。
真正让我头疼的是另一种情况:明明模型没问题,求解结果也不错,但解出来的最优解“看起来很怪”。比如有些变量变成 0、某些约束正好等于边界值,这种情况有时是退化问题。退化的意思是可行域的某些顶点重合了,导致最优点不唯一。碰到这种情况,x可能因为求解器内部选择的路径不同而不同,但fval应该是一致的。
我自己的处理方式:如果解不唯一,可以尝试给目标函数加一个极小的正则项,例如加上1e-6 * sum(x)。这样可以打破顶点重合时的平衡,让求解器给出一个更“居中”的解。这个技巧不能乱用,正则项太大会改变问题本身,太小心又没效果,需要自己调一个合适的量级。
4.2 大规模问题:稀疏矩阵、选项设置与冷门技巧
当变量数从十几个涨到几千个、几万个时,之前写的小脚本很可能撑不住。倒不一定是内存直接爆掉,而是密密麻麻的zeros矩阵会让你白白浪费大量内存和计算时间。这时候第一个建议是:使用稀疏矩阵。
Matlab 里构造稀疏矩阵最简单的方式之一是用sparse(i, j, v, m, n)直接按非零元素填:
iRow = []; jCol = []; vals = []; for k = 1:10000 % 根据你的问题结构添加非零元素 iRow(end+1) = ...; jCol(end+1) = ...; vals(end+1) = ...; end A = sparse(iRow, jCol, vals, m, n);稀疏矩阵在做矩阵乘法时只处理非零元素,效率高一个量级。linprog对内点法这类算法天然支持稀疏矩阵,所以传递进去不用做任何转换。
第二个建议:把大规模问题的选项设置得足够合理。默认算法不一定最适合你的数据形态。如果你有很多等式约束、很多零元素,'interior-point'可能会比'dual-simplex'快不少;反之,如果问题规模不算大但你希望每一步都能看到迭代情况,双单纯形法更好用。不要照抄别人的配置,一份数据跑一遍,对比两种算法的耗时和内存占用,把结果记下来,以后再遇到类似问题就能直接选最优方案。
第三个建议来自一个我实际遇到的场景:同一个约束矩阵被反复使用,但目标函数每次都要换一批。例如,采购价格每个月变一次,需求也每个月不同,但仓库容量和门店布局不变。这种情况下完全没必要每次重新生成A和Aeq再送进linprog。把约束矩阵提前算好存成.mat文件,求解时只更新f、b、beq,能省掉大量重复建模时间。
还有一个经常被忽略的小技巧:直接用output里的iterations看迭代次数。如果你发现求解很快,但迭代次数异常少,介入人为检查。尤其是当结果刚好卡在一个极端边界值时,多留一个心眼,看看是否真的符合业务直觉。数值求解器给你的“最优解”,也要放到真实场景里再过一遍脑子,这是所有优化从业者的基本素质。
这些年做下来,我用 Matlab 解过的线性规划问题少说也有几十种,小到课堂练习,大到实际项目里的排产与调度。每次能顺畅跑出结果的项目,几乎都有一个共同特征:前期建模和矩阵构造花了大量时间,求解器本身反而没怎么折腾。反过来,那些一上来就想直接套linprog的项目,多数会在一堆报错信息里反复绕圈。所以我最后再强调一次:linprog只是个翻译器,而真正有价值的,是你把现实问题翻译成数学表达式的那套本事。希望这篇内容能帮你少踩几个我踩过的坑,遇到类似问题时知道从哪里下手。