这套数学建模Matlab算法系列的教程,我在草稿箱里存了二十章的稿子,一直没想好怎么把目标规划这一章讲得不那么“教材腔”。原因很简单,线性规划在建模题里已经被用得烂熟,可一旦遇到“既要利润高,又要加班少,还想库存稳”这种多目标问题,很多同学就直接懵了:目标函数到底写哪一个?这恰恰是目标规划存在的意义——它不追求数学意义上的“唯一最优解”,而是追求在多个指标之间的“让各方都尽量满意”。这篇就来把目标规划的建模逻辑和Matlab实现彻底说透,文末的生产决策案例可以直接当模板用。
1. 线性规划解决不了“鱼和熊掌”,目标规划补上了哪块拼图
1.1 单目标与多目标的本质差异
线性规划有个非常鲜明的假设:所有约束都必须是铁律,目标函数只有一个,然后在这个铁律围成的可行域里找极值点。实际做数学建模题时,能把问题简化成这种单目标场景的其实很少。我见过太多队伍拿到题目第一件事就是合并目标——把利润、质量、时间统统折算成一个综合评分,折算完才发现量纲混乱、权重吵了一晚上也没定下来。
多目标决策的真实特征是什么?是目标之间互相拉拽。比如你想让产量最大,设备就可能超负荷;想让设备负荷均衡,利润可能就达不到预期。这时候不存在一个点能让所有目标同时达到“绝对最优”,只存在一个让你在优先级上做出取舍之后“足够好”的解。目标规划(Goal Programming)就是为这种取舍提供一套形式化语言的工具。
它的核心思想是:给每个目标设定一个“期望值”(目标值),然后允许目标实际达到的值与期望值之间存在正偏差或负偏差。最终要做的不是最大化某个目标,而是最小化这些偏差的总和。换句话说,你用“妥协”代替了“最优”,用“尽量接近”代替了“必须达到”。
1.2 三个核心概念:目标值、偏差变量、优先级
我第一次接触目标规划时,觉得最关键的突破点不是数学公式,而是三个概念的建立。
第一个是目标值。就是你希望这个指标达到什么水准,比如“利润至少70万”“总工时尽量不超过36小时”“产量尽量稳定在16吨”。这个目标值本身不是硬约束,而是一个“软约束”,你可以超一点,也可以差一点,关键看偏差有多疼。
第二个是偏差变量。为了让“差多少”能被数学表达,对同一个目标要引入两个非负变量:d+表示超过目标值的部分,d-表示低于目标值的部分。比如利润目标可以写成6*x1 + 4*x2 + d- ≥ 70,这里的d-表示利润缺口;工时目标可以写成4*x1 + 2*x2 - d+ ≤ 36,这里的d+表示超出的工时量。注意,理论上同一个目标的两个偏差变量不会同时为正,因为没有任何理由在一个目标上既超额又不足。
第三个是优先级。当你真正开始建模型时,会发现不同目标的重要性根本没法用同一把尺子衡量。比如利润少1万和加班多1小时,能直接相减吗?不能。所以目标规划允许你给目标划分优先级:第一优先级必须绝对满足,在这个前提下再考虑第二优先级,这就是分层思想。当然,也可以给每个偏差项配一个权重,用数值让“重要程度”变得可计算。
理解这三板斧,后面看代码就不会发怵了。所有目标规划的Matlab实现,本质上都是在“偏差变量最小化”和“优先级约束”这辆车上做文章。
2. Matlab求解目标规划的三条技术路线
写代码之前先把路线看清楚。我见过不少人一上来就找现成的fgoalattain函数,结果函数参数含义都没搞明白,算出来的解自己都不敢信。Matlab求解目标规划,主流有下面三条路,适用场景完全不同。
2.1 路线A:分层序列法(字典序优化)
这是最符合目标规划原始定义的做法,也最适合数学建模竞赛。基本思路是:把目标按优先级从高到低排好,然后一阶段一阶段地解线性规划。
第一阶段只优化第一优先级的偏差变量,得到最优解后,把这个偏差的最小值记录为val1。第二阶段在原问题基础上,增加一条约束“第一优先级的偏差必须小于等于val1”,再优化第二优先级的偏差变量。第三阶段同理,把第一层第二层的目标值都锁死,再去优化第三层。如此一层层往下压,直到所有优先级都处理完。
这样做的好处是逻辑透明、每一步都可以单独检查。我在实际建模题里,超过八成情况都用这条路线。它也能避开设权重的麻烦,因为优先级的“强制先后”关系不需要你回答“1万元等于几小时”。
2.2 路线B:加权系数法
加权法其实更简单粗暴:把每个偏差变量乘上系数后放进同一个目标函数,比如min z = w1*d1- + w2*d2+ + w3*(d3- + d3+),然后当成一个普通线性规划去求解。Matlab里直接用linprog就能做,不需要分层迭代。
但这里有一个隐藏得很深的坑——权重和量纲。假设利润单位是万元,工时单位是小时,如果你随手把两个偏差变量权重都设为1,那就等于默认“少1万元利润”和“多1小时工时”的罪恶感是一样的,这在现实中几乎不可能成立。我个人的经验是,除非你有十足的把握对不同指标进行量纲归一化,否则少用加权法去做多目标建模,它更适合指标之间本身单位相同、数值范围差不多的场景。
2.3 路线C:工具箱自带的fgoalattain多目标达成函数
如果题目里的目标数量多、且它们之间没有明显的优先级高低,可以试试优化工具箱里的fgoalattain。它的机制是给定每个目标的期望值goal和一个权重向量weight,然后求解一个加权最小最大问题,核心思路是让实际值与目标值的偏差按比例控制。
这个函数的优点是省事,不需要手动构造偏差变量;缺点是它默认处理“不超过目标”的方向,如果你想让某个指标“接近某个值”,需要把该指标拆成“从上方接近”和“从下方接近”两个方向处理。而且它的权重向量选择依然有玄学成分。我把这三条路线整理成了下面这张对比表:
| 方法 | 优先级表达 | 实现难度 | 适合场景 |
|---|---|---|---|
| 分层序列法 | 绝对优先级,先后次序强制 | 中等,需多次调用linprog | 目标之间有明确轻重缓急,适合建模竞赛 |
| 加权系数法 | 用数值权重综合比较 | 低,一次linprog搞定 | 指标同量纲、可量化比较 |
| fgoalattain | 权重比例+最小最大策略 | 低,单函数调用 | 目标多、互相权重不明显、想快速出结果 |
我个人建议:先把分层序列法吃透。只有当你真的理解每一层到底在锁死什么,才能看懂fgoalattain内部在做什么。
3. 一个完整案例:生产计划目标规划的实战推演
空谈概念没有用,我们直接看一道典型的建模题,从文字变成Matlab代码,一步一步来。
3.1 问题背景与数字化建模
某车间计划生产A、B两种产品。生产单位产品的利润分别记为6和4;生产消耗的原料分别是2和3,一周可用原料最多48个单位;生产消耗的设备工时分别是4和2,一周设备最多40小时。这些是硬约束,完全没有商量余地。
现在管理层又提了三个目标:
- 本周利润至少达到
70(这个目标优先级最高); - 设备工时尽量控制在
36小时以内,因为超过后需要支付加班费; - 总产量尽量保持在
16附近,多了可能仓库堆不下,少了又担心交不上货。
注意第一目标是“至少”,第二目标是“不超过”,第三目标是“接近某个值”。三种目标形式我都塞进了同一个模型里,就是为了示范偏差变量在不同场景下分别怎么设。
令决策变量为x1、x2,分别表示A、B产品产量。硬约束写成:
2*x1 + 3*x2 <= 48 4*x1 + 2*x2 <= 40利润目标引入负偏差变量d1m,表示利润缺口:
6*x1 + 4*x2 + d1m >= 70工时目标引入正偏差变量d2p,表示工时的超额量:
4*x1 + 2*x2 - d2p <= 36产量目标比较特殊,需要同时引入正负偏差,让实际产量夹在16的左右两侧:
x1 + x2 - d3p + d3m <= 16 x1 + x2 - d3p + d3m >= 16等价于两个不等式:x1 + x2 - d3p + d3m <= 16和-x1 - x2 + d3p - d3m <= -16。
3.2 偏差变量的引入与约束矩阵搭建
上面这些式子看着挺多,但整理成矩阵其实很工整。我习惯把决策变量固定为下面这个顺序:
x = [x1; x2; d1m; d2p; d3m; d3p]那么所有不等式约束就可以拼成一个大的系数矩阵A和一个右侧向量b:
A = [2 3 0 0 0 0; 4 2 0 0 0 0; -6 -4 -1 0 0 0; 4 2 0 -1 0 0; 1 1 0 0 -1 1; -1 -1 0 0 1 -1]; b = [48; 40; -70; 36; 16; -16];注意第三行,为了把6*x1 + 4*x2 + d1m >= 70转成A*x <= b的标准形,左右两边同时乘了负号,所以是-6*x1 - 4*x2 - d1m <= -70。很多新手在这里栽跟头,一不留神不等式方向就反了。
3.3 完整Matlab代码及注释
下面是分层序列法的完整实现。第一层只优化利润缺口d1m;第二层锁死d1m的方法是在矩阵后面追加一行0 0 1 0 0 0,等号右侧写第一层的最优值;第三层再追加一行锁d2p,最后优化产量偏差。
% 目标规划分层求解 Demo % 决策变量顺序:x1, x2, d1m, d2p, d3m, d3p % d1m: 利润缺口; d2p: 工时超额; d3m,d3p: 产量正负偏差 A = [2 3 0 0 0 0; 4 2 0 0 0 0; -6 -4 -1 0 0 0; 4 2 0 -1 0 0; 1 1 0 0 -1 1; -1 -1 0 0 1 -1]; b = [48; 40; -70; 36; 16; -16]; lb = zeros(6,1); ub = []; options = optimoptions('linprog', 'Display', 'none'); % ---- 第一层:优先满足利润目标 ---- f1 = [0; 0; 1; 0; 0; 0]; % 只最小化 d1m [x1, val1] = linprog(f1, A, b, [], [], lb, ub, options); d1m_star = x1(3); fprintf('P1 阶段:最小利润缺口 d1m = %.4f\n', d1m_star); % ---- 第二层:在 P1 基础上,最小化工时超额 ---- A2 = [A; 0 0 1 0 0 0]; % 新增约束:d1m <= d1m_star b2 = [b; d1m_star]; f2 = [0; 0; 0; 1; 0; 0]; % 只最小化 d2p [x2, val2] = linprog(f2, A2, b2, [], [], lb, ub, options); d2p_star = x2(4); fprintf('P2 阶段:最小工时超额 d2p = %.4f\n', d2p_star); % ---- 第三层:在 P2 基础上,最小化产量总偏差 ---- A3 = [A2; 0 0 0 1 0 0]; % 新增约束:d2p <= d2p_star b3 = [b2; d2p_star]; f3 = [0; 0; 0; 0; 1; 1]; % 最小化 d3m + d3p [x3, val3] = linprog(f3, A3, b3, [], [], lb, ub, options); fprintf('P3 阶段:最小产量偏差 = %.4f\n', val3); disp('最终决策变量(顺序含义见注释):'); disp(x3);这段代码跑完后,你会看到第一层d1m = 0,说明利润70这个目标可以不打折扣地满足;第二层d2p = 1,说明在保证利润的前提下,工时至少要超出目标1小时;第三层产量偏差是0.5,说明总产量没法正好落在16上。最终解大致是x1=2, x2=14.5,利润正好70,总产量16.5,工时37小时。
3.4 结果解读:为什么第二层的最小值不等于零也正常
拿到结果先不要急着怀疑代码。d2p=1意味着什么?意味着“利润至少70”和“工时不超过36”这两个目标之间本身就存在不可调和的冲突。你硬要同时满足,就需要用第二优先级去妥协。很多同学看到某个层的偏差不是0就慌了,其实这是目标规划最正常的输出——偏差值本身就是决策的一部分,它告诉你哪些目标在现实中需要让步,让步多少。
从最终解也能反过来验证:x1=2, x2=14.5时,利润确实等于70,工时37小时超出1小时,总产量16.5离16差0.5。三个指标的矛盾清晰呈现,决策者可以根据这个结果去和车间谈:“要么接受37小时加班,要么把利润预期稍微降到68看看能不能换回产量稳定。”这就是目标规划相比普通线性规划最大的价值——它给你一套可视化的妥协方案。
4. 实战中的常见翻车点,我一个个帮你排除
目标规划的解题框架其实不复杂,但实际做建模题时,隐藏问题全在细节里。下面这几个坑我基本每次带人做都会遇到,提前排掉能省一晚上调试时间。
4.1 量纲不一致导致的权重失效
用加权系数法时,最经典的翻车就是拿“万元利润”和“小时工时”直接乘权重。你的目标函数里,利润偏差量级可能是0~10,工时偏差量级可能是0~100,那在优化器眼里,它天然会优先减小数值大的那一项,权重的真实含义被量纲彻底吞掉。
解决思路有两种:一是把所有目标度的值除以各自的目标值,变成相对偏差;二是在写权重前先跑一遍不带权重的模型,看看每个偏差变量的自然范围,然后让权重与范围成反比。绝对不要随手填w=[1,1,1]就算完事。
4.2 优先级搞反之后的最优解“变形”
分层序列法里,优先级的顺序直接决定了解落在哪个角落。我试过一个案例,第一优先级本来是“成本不超标”,第二优先级才是“完成订单”;结果代码里顺序写反,算出来的解成本确实漂亮,订单数量却差了一大截。因为优化器严格执行了你给的顺序,它不会觉得“订单重要性更高”,它只会机械地先把第一层锁死。
排查方法很简单:把每一层解出来的决策变量打印出来,逐层人工检查一下目标值是否真的满足上一层的约束。如果发现某一层的结果让上一层的偏差变大了,几乎可以肯定是约束追加时写错了行。
4.3 模型无解时先别急着断言“题目错了”
有一次做竞赛模拟题,我在第一层就直接报出“No feasible solution”,当时第一反应是数据有问题,结果排查了半天才发现,是自己在建利润目标时把约束方向写反了,>=70写成了<=70。这种事非常常见,尤其是约束一多,手一抖方向就反了。
另一个隐蔽的不可行来源是多个偏差变量互相锁死。比如第三层目标“产量尽量接近16”,你一旦写错符号,把d3m和d3p的系数弄颠倒了,模型就可能出现既要求产量不超过16又要求产量不低于16、但实际又达不到的处境。遇到无解,我通常先把所有偏差项系数清零跑一次,看基础约束本身有没有解;如果有,再一条一条把软约束加回去,二分定位是哪条出了事。
4.4 偏差变量“只设一半”导致目标失真
利润目标是“至少70”,只需要设负偏差d1m;工时目标是“不超过36”,只需要设正偏差d2p。但产量目标是“接近16”,这种情况如果你只设一个方向,比如只设d3p,那模型会自动把产量压到16以下并且没有任何成本,这根本不是你想要的。
所以在写约束时,先问自己三个问题:这个目标允许超还是允许低?还是两边都不允许?两边都不允许就必须同时引入d-和d+,且目标函数要把两者都最小化。这个看似基础的习惯,能预防掉大半的建模错误。
5. 从“会解”到“会用”的进阶思路
代码跑通只是第一步,真正到数学建模竞赛或者实际项目里,目标规划还会遇到两个绕不开的进阶问题:整数决策变量和目标不可达时的策略。
5.1 与整数规划结合的细节
生产计划里的产量变量经常会有整数要求,比如产品只能按箱生产,不能出现14.5箱这种结果。普通linprog没法处理整数,你需要把求解器换成intlinprog。分层序列法的框架不用变,每层的目标函数f1、f2、f3同样适用,只需要额外告诉求解器哪些变量是整数。
我在处理这类问题时,会把x1、x2设为整数变量,偏差变量保持连续。代码改动其实非常小:intcon = [1,2];然后调用intlinprog(f, intcon, A, b, [], [], lb, ub, [], options)。需要提醒的是,加了整数约束后每一层的最优解可能需要多花一些时间,但求解思路没有任何变化。
5.2 多个目标互相冲突时的实际取舍策略
有时候你会发现,无论怎么调优先级,某个目标的偏差都压不下去。比如市场需求波动大,你要库存低、又要缺货少,这两个目标天然对立。这种时候我的做法是:先跑一版分层求解,把每一层的偏差值都记下来,然后做一组灵敏度分析——把最高优先级的目标值往下调5%、10%,看第二第三目标的偏差改善量。
这个过程的目标规划本身可以帮你建立一张“妥协代价表”:比如利润目标从70降到66,工时偏差能从1小时降到0.5小时。财务或者管理层看到这张表,通常马上就能拍板。这就是目标规划最有说服力的一个应用方式,它不是在替你决策,而是在帮你把决策的代价摆在台面上。
我在一次实际项目中,就这样来回跑了十几轮linprog,最后发现把第一目标稍微放宽2%,第二目标的偏差能下降78%,当时客户直呼这个分析比直接给个“最优解”有用多了。这也是我个人特别偏爱目标规划的原因:模型越贴近真实决策,它的结果越能被人接受。下次如果你拿到一道多目标建模题,别急着合并指标,先把每个目标的偏差变量写出来,然后一层一层锁着解。这个方法可能不会给你一个数学上最漂亮的“极值”,但它给你的,一定是最能解释得通的答案。码完这一段,我自己又拿这个案例跑了一遍代码,看到输出里的val3=0.5,还是觉得目标规划这种“把妥协算明白”的思路,才是建模题里真正吃经验的地方。