
1. 目标规划在数学建模里到底有什么用多数数学建模教材会把“目标规划”放在线性规划之后、非线性规划之前不是没有原因的。线性规划解得再漂亮一碰到真实题目里“又要利润高、又要加班少、又要库存低”这种多头拉扯的要求就明显僵住了。目标规划正好补上这块短板它不追求单一目标下的最优而是追求把一堆互相打架的期望值按优先级逐个满足。这篇文章就围绕数学建模里的经典章节“目标规划”把它的数学模型、Matlab算法实现、实战案例和踩坑经验一次性讲透不管你是刚读到这一章的新手还是马上要上赛场的老手都可以对照这篇直接操作。1.1 线性规划的尴尬为什么一个目标不够用先说个很经典的生产计划场景。假设你是一个工厂的排产负责人手里有两种产品A单价高但耗料多B单价低但走量快。你想让利润最大这是典型的线性规划问题目标函数只有一个约束条件全是硬性的比如原料不超过50千克、工时不超过120小时求解器一跑答案就出来了。但现实问题往往不是“利润最大”四个字能说清楚的。销售部要求A产品产量尽量接近15件因为下个月有客户订单车间要求设备工时尽量满负荷运转不然绩效考核过不去财务又说库存积压要控制产量别冲太猛。这些诉求互相矛盾利润最大的方案可能让A产品产量偏离订单目标很远完全按订单生产又可能让设备闲置。这时候如果继续用线性规划就只能硬着头皮把所有目标加权成一个目标函数比如“利润×0.6 产量达成率×0.3 工时利用率×0.1”这就是我特别不推荐的做法因为各个指标的量纲不一样利润是元产量是件工时是小时强行加在一起权重系数根本没法让评审信服决策者也说不清这权重到底是怎么拍出来的。目标规划的思路完全不同它不要求把所有目标揉成一个而是允许每个期望值独立存在再用“偏差变量”去度量每个期望值差了多少。这样一来排产问题就变成“先保证利润至少达到某个数再尽量照顾A的订单量有余力再考虑工时饱和”层层递进逻辑非常干净。1.2 目标规划解决什么问题目标规划Goal Programming简称GP处理的是多目标且目标之间存在冲突的决策问题。它的核心动作有两个把每个期望目标写成一条目标约束允许被突破但突破的幅度要计入偏差变量给目标排优先级第一优先级的偏差先优化到最小再在不损害第一优先级的前提下优化第二优先级依次往下。这和线性规划的区别非常直观线性规划的约束是“硬”的违反就不可行目标规划的约束是“软”的违反一点没关系但要在目标函数里付出代价。实际比赛里这种软约束的建模方式非常吃香因为现实中几乎没有哪个决策是“绝对不能妥协”的目标规划恰好把“妥协程度”量化了。它特别适合的场景包括生产排产、投资组合、物流调度、人力资源分配、供应链平衡这类“多目标相互冲突”的优化题。国赛、研赛里常出现的“资源配置类”题目只要题目里出现了“尽量”“希望”“优先”“重点考虑”这类字眼都可以优先想到目标规划。1.3 目标规划在竞赛中的使用场景从我参加竞赛和评论文的经验看目标规划在论文里的“性价比”很高。它不像启发式算法那样要调一堆参数也不像深度学习那样需要大量数据只要建模逻辑清晰用Matlab的linprog或者fgoalattain就能稳定求解而且结果非常容易解释。很多优秀论文会把目标规划作为主模型再配合灵敏度分析、方案对比形成完整的决策链条。比如国赛经典的“产品生产计划”“医院病床安排”“快递网点选址”等题目目标规划都能找到合适的落点。尤其是题目明确给出“希望”“尽可能”等模糊要求时直接把模糊词翻译成偏差变量在评审眼里就是实打实的建模能力。2. 模型的数学结构与容易绕晕的几个概念这一部分我建议你慢点读因为目标规划的所有算法实现都建立在下面这套符号体系上。一旦符号搞混Matlab代码里的行向量、列向量、等式约束就会跟着乱套。2.1 一个完整的目标规划模型长什么样目标规划的通用写法长这样决策变量x₁, x₂, …, xₙ绝对约束硬约束Σ a_ij x_j ≤ b_i或 ≥、目标约束软约束Σ a_ij x_j d_i⁻ - d_i⁺ g_i其中 g_i 是第 i 个目标的期望值d_i⁻ 是负偏差表示实际值比期望值少多少d_i⁺ 是正偏差表示实际值比期望值多多少。目标函数min Z Σ P_l ( w_j⁻ d_j⁻ w_j⁺ d_j⁺ )这里 P_l 是优先因子w_j⁻ 和 w_j⁺ 是同一优先级下各偏差变量的权重系数。举例来说如果目标是“A产品产量尽量达到15件”那就写成x₁ d₂⁻ - d₂⁺ 15如果实际产量是12那么 d₂⁻ 3d₂⁺ 0如果实际产量是17则 d₂⁻ 0d₂⁺ 2。目标函数里如果想让产量正好接近15就把 d₂⁻ 和 d₂⁺ 一起极小化如果只担心产量不足就只极小化 d₂⁻。2.2 绝对约束、目标约束和偏差变量的关系很多新手最困惑的一点是绝对约束和目标约束到底有什么区别我的理解是绝对约束是“红线”不能越过越过了模型直接不可行目标约束是“期望线”可以上下浮动但浮动要产生偏差变量。举个例子。原料不超过50千克这是绝对约束2x₁ 1.5x₂ ≤ 50原料一旦超过就是违法方案。而利润不低于1000元是目标约束40x₁ 30x₂ d₁⁻ - d₁⁺ 1000利润低于1000时 d₁⁻ 为正说明目标没完成利润高于1000时 d₁⁺ 为正说明超额实现。这里还有一个非常关键但经常被误解的细节d₁⁻ 和 d₁⁺ 能不能同时大于0从数学定义上看同一约束的实际值不可能既低于期望值又高于期望值所以二者必然至少有一个等于0。但在建模时不要手工去添加 d₁⁻ × d₁⁺ 0 这种非线性约束因为在线性规划中完全没有必要。目标函数里 d₁⁻ 和 d₁⁺ 的系数都是正数求最小值时最优解会自动让其中一个为0如果两个都为0说明恰好达到期望值那正是我们最想要的结果。2.3 优先因子与加权系数的正确理解优先因子 P₁、P₂、P₃ 表达的是“字典序”关系P₁ 对应的目标比 P₂ 重要得多不是“P₁ 比 P₂ 重要两倍”这种比例关系而是“先解完第一层再去解第二层”。这一点在算法上极其重要。有些教材会建议用很大的数值权重把目标规划“压”成一个普通线性规划比如令 P₁ 10000P₂ 100P₃ 1。这种方法能跑出结果但老实说隐患不小。万一某个第一优先级指标亏损了0.1个单位而第二优先级里有1000个偏差需要补偿加权后的结果可能就牺牲第一优先级去讨好第二优先级了这违背了决策者“优先保证最重要目标”的本意。所以我强烈建议用分层序贯法而不是一次性加权。同一优先级内部的加权系数则要根据实际情况来定。比如目标是“A产量尽量达到15B产量尽量达到10”如果你觉得A比B重要一些可以写成 min (d₂⁻ d₂⁺) 2(d₃⁻ d₃⁺)表示B的偏差重要性更高或者反过来。加权系数反映的是决策偏好没有绝对对不对关键是让评审看懂你的偏好设定逻辑。3. Matlab实现序贯式分层求解才是主流打法目标规划的Matlab实现我见过三条路线一是直接把所有优先级转换成大权重后丢给linprog二是用fgoalattain工具箱三是我最喜欢的按优先级分层、循环调用linprog。第三条路线最贴合目标规划的理论在竞赛论文里也最好解释。3.1 为什么竞赛里多用linprog而不是fgoalattainlinprog是Matlab自带的线性规划求解器求解速度快结果稳定还能很方便地处理等式约束。目标规划的偏差变量全部是线性关系只要把目标规划转换成标准的线性规划形式linprog就是最顺手的选择。fgoalattain是专门用于求解多目标问题的工具箱函数它通过权重把多个目标组合成单一的度量然后逼近一个目标值向量。但它的一个明显短板是不支持严格的优先级分层或者说支持的代价很高。你必须把优先级关系强行转化为权重这又回到了“P₁到底该给多大”的难题。再加上fgoalattain本质上是调用序列二次规划算法求解的是非线性优化问题如果变量规模大一点收敛速度会明显慢于linprog。所以我的结论很明确只要你的模型是线性的就优先用linprog分层求解fgoalattain更适合那些目标函数本身带非线性、或者你只想要一个“折中解”来做对比的场景。3.2 第一层求解的标准代码模板先看一个最小可运行的例子。假设决策变量顺序为x₁A产量、x₂B产量、d₁⁻、d₁⁺、d₂⁻、d₂⁺、d₃⁻、d₃⁺目标约束有三条40x₁ 30x₂ d₁⁻ - d₁⁺ 1000利润目标 x₁ d₂⁻ - d₂⁺ 15A产量目标 x₂ d₃⁻ - d₃⁺ 10B产量目标绝对约束有两条2x₁ 1.5x₂ ≤ 50材料约束 3x₁ 2x₂ ≤ 120工时约束第一层想极小化利润负偏差 d₁⁻也就是“利润尽量不低于1000”。Matlab代码如下clear; clc; % 决策变量顺序: x1 x2 d1- d1 d2- d2 d3- d3 A [2 1.5 0 0 0 0 0 0; 3 2 0 0 0 0 0 0]; b [50; 120]; Aeq [40 30 1 -1 0 0 0 0; 1 0 0 0 1 -1 0 0; 0 1 0 0 0 0 1 -1]; beq [1000; 15; 10]; lb zeros(8,1); ub []; % 第一层目标: min d1- c1 [0 0 1 0 0 0 0 0]; [x1, f1, exitflag1] linprog(c1, A, b, Aeq, beq, lb, ub); if exitflag1 0 error(第一层求解失败请检查模型是否可行); end这里有几个需要特别说明的地方。第一linprog求解的是最小值问题所以目标函数系数向量c里需要在对应偏差变量的位置写1其他位置写0。第二Aeq和beq专门用来放目标约束因为目标约束是等式而A和b放的是绝对约束的不等式。第三lb设置为全零向量是因为决策变量和偏差变量都要求非负。第四ub不用设置默认无穷大即可。3.3 用循环把分层过程写成通用脚本第一层解完之后我们要把第一层的最优值锁住再解第二层。锁住的方法很简单把第一层的目标函数系数向量 c₁ 作为新的一行添加到Aeq里把f₁作为beq新增的一项。这样第二层的解空间就只包含“第一层已经达到最优”的方案。接着解第二层A产量尽量达到15即极小化 d₂⁻ d₂⁺。然后继续锁定第二层最优值再解第三层。用循环写的话整个分层过程可以封装得很干净% 各层目标函数系数 C {[0 0 1 0 0 0 0 0], ... % 第1层: min d1- [0 0 0 0 1 1 0 0], ... % 第2层: min d2- d2 [0 0 0 0 0 0 1 1]}; % 第3层: min d3- d3 zstar zeros(length(C), 1); for k 1:length(C) ck C{k}; [x, fval, exitflag] linprog(ck, A, b, Aeq, beq, lb, ub); if exitflag 0 error(第%d层求解失败, k); end zstar(k) fval; % 把当前层最优值锁定为等式约束 Aeq(end1, :) ck; beq(end1) fval; end x_final x; disp(最终决策变量 [x1 x2 d1- d1 d2- d2 d3- d3]); disp(x_final);这样写的好处是优先级数量增减非常方便。比如你还有第四层“设备工时尽量达到80”只要新建一个目标约束往C里再塞一行系数就行后面的循环完全不用改。注意一个细节分层时加入的约束是“当前层目标函数值等于最优值”也就是 ck * x fval。有些教程喜欢写“ck * x ≤ fval”因为目标是最小化小于等于能达到同样的锁定效果理论上也正确。但我个人更推荐用等号因为等号更直观后续方案必须严格达到上一层级的目标值不允许有任何退让。用小于等于的话特殊情况下可能出现数值精度问题导致后续层悄悄牺牲了一点点上一层目标虽然影响很小但没必要冒险。3.4 fgoalattain也可以但注意它的定位如果你非要用fgoalattain大概写法是这样的fun (x) [40*x(1) 30*x(2); x(1); x(2)]; goal [1000; 15; 10]; weight abs(goal); A [2 1.5; 3 2]; b [50; 120]; xf fgoalattain(fun, [5; 5], goal, weight, A, b, [], [], zeros(2,1), []);fgoalattain的理解方式是它把三个目标利润、A产量、B产量同时尽量往目标值靠weight用来控制每个目标的相对重要程度。它给出的解往往是一个“折中解”而不是严格的“先保证利润、再保证A产量、最后B产量”的字典序解。所以在竞赛论文里如果评审问你“这个解的优先顺序是怎么体现的”用fgoalattain就有点难解释。我会在最后对比方案、想快速看看多目标之间的权衡关系时才用fgoalattain做个参考主模型始终用linprog分层。4. 一个完整案例从题意到Matlab结果讲到这里我估计你已经跃跃欲试了。下面用一个我实际调试过的案例把从建模到出结果的完整过程过一遍重点是看结果怎么解读、回代检查怎么做。4.1 案例背景与目标拆解某厂生产A、B两种产品。A产品每件利润40元单件消耗材料2千克耗用工时3小时B产品每件利润30元单件消耗材料1.5千克耗用工时2小时。材料总量50千克工时总量120小时。决策层的目标有三个按优先级排列P₁利润不低于1000元P₂A产品产量尽量达到15件P₃B产品产量尽量达到10件。这个案例是我特意挑的因为材料约束相对紧张利润目标、产量目标之间存在明显的拉扯能看出分层求解的实际效果。4.2 建模和系数整理决策变量定义x₁A产品产量x₂B产品产量 d₁⁻利润负偏差d₁⁺利润正偏差 d₂⁻A产量负偏差d₂⁺A产量正偏差 d₃⁻B产量负偏差d₃⁺B产量正偏差。绝对约束材料2x₁ 1.5x₂ ≤ 50 工时3x₁ 2x₂ ≤ 120目标约束利润40x₁ 30x₂ d₁⁻ - d₁⁺ 1000 A产量x₁ d₂⁻ - d₂⁺ 15 B产量x₂ d₃⁻ - d₃⁺ 10目标函数按优先级拆成三层第一层 min d₁⁻ 第二层 min d₂⁻ d₂⁺ 第三层 min d₃⁻ d₃⁺4.3 代码与结果解读把上面整理好的系数填进Matlab用第3节的循环代码跑一遍。我实际跑出来的结果大体是这样的最终决策变量 [x1 x2 d1- d1 d2- d2 d3- d3] 15.0000 13.3333 0 0 0 0 0 3.3333解释一下。x₁ 15A产量目标完美达成所以 d₂⁻ 和 d₂⁺ 都是0x₂ 13.3333B产量比期望的10多了3.3333所以 d₃⁺ 3.3333d₃⁻ 0利润 40×15 30×13.3333 1000d₁⁻ 和 d₁⁺ 都是0第一优先级目标圆满实现。这个结果特别有意思B产品实际产量13.3333件不是3.3333件偏差体现在“超出目标”的方向上。你可能会问B产量超过10不是好事吗为什么第三层还会产生正偏差因为第三层追求的是“尽量达到10”不管超没超和目标越远偏差越大。如果决策者觉得“B产量超过10更好只要不超过产能上限就行”那第三层目标函数就应该改成 min d₃⁻而不是 min d₃⁻ d₃⁺。这就是建模时偏好在目标函数里的体现。4.4 回代检查与结果分析拿到结果以后回代检查是所有求解流程里最不能省的一步。把x₁ 15、x₂ 13.3333代回约束材料消耗 2×15 1.5×13.3333 30 20 50恰好用满材料约束这是好事说明没有浪费工时消耗 3×15 2×13.3333 45 26.6667 71.6667远低于120工时大量富余。所以这个方案的本质是“材料拖了后腿”。如果还想提高利润就必须增加材料供应只要材料约束卡在50千克利润最高也就是1000元A产量达到15、B产量冲到13.3333就是极限。这个结论直接引出了敏感性分析的方向把材料上限从50改成55、60重新跑一遍模型看看利润能抬到多少、B产量能改善多少。比赛里这样做一段“材料供应量的敏感性分析”论文质量会明显提升因为你不是只给一个孤立的解而是给出了决策者可以操作的杠杆。5. 常见坑位与现场经验目标规划模型写起来不难但我在实际调试和代赛过程中见过太多在这几个位置翻车的情况。单独开一节把坑都排一遍。5.1 建模阶段最容易踩的四个坑第一个坑把目标约束写成不等式。比如“利润不低于1000”有人直接写成 40x₁ 30x₂ ≥ 1000然后就没有偏差变量了。这在纯线性规划里没问题但在目标规划里不行因为一旦写成硬不等式利润目标就变成了绝对约束无法分层优化。正确写法必须把软目标写成等式加偏差变量。第二个坑偏差变量的系数符号写反。目标约束的标准形式是“实际值 d⁻ - d⁺ 目标值”很多人会把正负号搞反导致d⁺和d⁻的含义完全颠倒最后结果怎么看都不对。我的记忆方法是负偏差是指“还不够”所以它作为“补偿项”加在左边把实际值顶到目标值正偏差是指“超出了”所以要减掉它才能回到目标值。第三个坑在不该加的地方加了绝对值或非线性项。有些同学想表达“实际值离目标值越近越好”就直接写 |实际值 – 目标值|然后丢给Matlab去算。这在线性模型里完全没必要因为 |实际值 – 目标值| 用 d⁻ d⁺ 就能线性表示。一旦写绝对值模型就从线性规划变成了非线性规划求解难度上去一大截结果还不一定比线性版本好。第四个坑给同一优先级下的偏差变量设置权重时完全不管量纲。同一优先级内如果目标是“利润偏差”和“产量偏差”同时最小那么利润偏差的单位是元产量偏差的单位是件如果不加权重直接相加相当于默认“1元利润偏差 1件产量偏差”的兑换率这个隐含假设很可能不是决策者的本意。我建议在处理这类混合目标时把权重设定依据写进论文里比如用期望值的倒数做归一化或者明确说明“利润偏差更重要权重取2”至少要让评审看见你思考过这个问题。5.2 linprog调用细节备忘linprog的参数顺序是固定的我经常看到有人在A和Aeq的位置上传错矩阵。在只做目标规划时建议养成一个习惯先写下“变量顺序表”再写A、b、Aeq、beq最后写c。变量顺序表就像代码注释一旦行列对不上对着变量顺序表能一眼看清。另外linprog返回的exitflag只有等于1时才表示求解成功。如果exitflag小于等于0常见原因有两种一是模型真的不可行比如某个绝对约束把可行域彻底堵死了二是某一层的锁定约束与原有约束冲突说明上一层的目标值在当前约束条件下不可能实现。遇到这类情况先把绝对约束放宽或者检查是否把某一层的目标值写错了。matlab的linprog默认用的是对偶单纯形法对大部分中等规模问题都足够快。如果你的决策变量数量特别大比如超过几万可以考虑换成linprog内置的interior-point-legacy算法但竞赛题一般到不了这个规模不用过度优化。5.3 竞赛现场的实战建议到赛场上的时候时间紧、节奏快我给出三个比较实用的建议。第一分层求解的层数不要搞太多。三到四层是评审最容易接受的范围层数太多反而会让模型显得琐碎。处理含有很多目标的题目时先分类归并把同类目标合并成一个优先层级再设定层内权重。第二每一层求解完以后把中间结果打印成表格写进论文的附录或正文。比如做一个三列的表优先级、该层目标函数最优值、对应关键决策变量取值。这个表格能非常直观地展示“逐层优化”的过程评审一看就懂。第三结果出来以后先做简单手算验证。我在答辩现场见过不少同学被评委问第一句话就问懵你的方案代回硬约束满足吗如果你提前手算过一遍代回检查就十秒的事但现场表现完全不一样。6. 最后再说几句实在话目标规划这套方法模板化程度很高但真正让它出彩的地方不一定在算法而在把决策者的“优先级”翻译成数学语言的过程。同一个问题有人把利润放第一优先有人把交期准时率放第一优先得到的结果可能完全不同这种差异不是谁对谁错而是决策偏好不同。竞赛里清晰地把偏好建模过程写出来往往比堆砌复杂算法更能拿分。我自己的习惯是在正式用Matlab求解之前先在纸上把每一层的目标函数系数和约束矩阵逐个写出来反复对照变量顺序表检查两遍。因为分层求解一旦层数多了错一个符号后面的层全跟着错排查起来非常痛苦。很多题目看起来复杂但拆到最后都是目标规划壳子里的线性结构把基础概念吃透Matlab的调用反而是最简单的一步。这一套方法你复现两三次以后就会发现它已经是顺手就能用的常规武器了。