吃拼好饭可能会不健康,那么自己做呢?如果我们要自己定制一套健康餐,那么成本呢?假设今天阿坤要在兼顾健康和成本的情况下尝试规划一套合适的健康套餐,规划的方法有很多种,层出不穷,不胜枚举,它希望找一种轻松省心的方式进行规划......
一. 题目
某学校为学生提供营养套餐,希望以最小费用来满足学生对基本营养的需求.按照营养学家的建议,一个人一天对蛋白质、维生素 A和钙的需求如下:50g蛋白质、4000 IU维生素 A 和1 000 mg钙。我们只考虑以下食物构成的食谱:苹果、香蕉、胡萝卜、枣汁和鸡蛋,其营养含量见下表.确定每种食物的用量,以最小费用满足营养学家建议的营养需求,并考虑:
(1)对维生素A的需求增加1个单位时,是否需要改变食谱?成本增加多少?如果对蛋白质的需求增加1g 呢?如果对钙的需求增加 1m g呢?
(2)胡萝卜的价格增加1角时,是否需要改变食谱?成本增加多少?
食物营养成分表
食物 | 单位 | 蛋白质 (g) | 维生素 A (IU) | 钙 (mg) | 价格(角) |
---|---|---|---|---|---|
苹果 | 138 g/个 | 0.3 | 73 | 9.6 | 10 |
香蕉 | 118 g/个 | 1.2 | 96 | 7 | 15 |
胡萝卜 | 72 g/个 | 0.7 | 20,253 | 19 | 5 |
枣汁 | 178 g/杯 | 3.5 | 890 | 57 | 60 |
鸡蛋 | 44 g/个 | 5.5 | 279 | 22 | 8 |
二. 思路解析
1.前导
① 模型选取
面对这样一个问题,由于涉及的变量组有五组,我们光凭瞪眼硬算,显然是很难得出结果的。那么我们应该怎么入手呢?我们观可以观察到,在满足一定营养需求的条件下,选择食物数量使总成本最小,这是求最小化的最优方案问题,而它的目标函数是线性的(总成本 = 各食物数量 × 单价),再看以下限制的要求,可以发现它的限制条件也是线性的(蛋白质总量 ≥ 50g,维生素 A 总量 ≥ 4000 IU,钙总量 ≥ 1000 mg),这些限制(约束)和目标都可以说是对变量(食物用量)的线性函数,所以我们可以想到要用线性规划来建模求解。
②线性规划概述
简要的定义就是,线性规划(Linear Programming,简称LP)是一种用来求解在一组线性约束条件下,使线性目标函数达到最大值或最小值的问题的数学优化方法
问题通常包括三个要素,决策变量、目标函数还有约束条件:
-
首先是目标函数,即一个线性函数,我们最终需要把它最大化或最小化。
比方说一个线性表达式:
;这里的 z 是目标函数,
则是常数系数,
是变量。
-
至于约束条件呢,就是一组或多组线性不等式/等式,用于限制变量的取值范围。
比方说:
当然通常还会有非负性约束,也就是 ,变量通常被限制在非负区间,这叫可行区域。
-
最后就是决策变量,即表可选择且可调整的决策对象,用变量形式的表达,也就是:
这个指代的是问题中要确定的数量(产品产量、资源分配量、食物数量)
2.模型构建
①决策变量
题中有五种食物的选择,那我们就设定每种食物的数量为决策变量:
-
:苹果数量 /个
-
:香蕉数量 /个
-
:胡萝卜数量 /个
-
:枣汁数量 /杯
-
:鸡蛋数量 /个
②目标函数
既然求的是最优方案的总成本,那么目标函数的常数系数也就只能是每个食物对应的价格了:
总费用
③约束条件
1) 蛋白质
推导过程:
- 苹果每个0.3g
- 香蕉每个1.2g
- 胡萝卜每个0.7g
- 枣汁每杯3.5g
- 鸡蛋每个5.5g
将这些食物的贡献计数汇总,加总使得总蛋白质计数至少50g,构成一个不等式。
公式:
2) 维A
推导过程:
- 苹果73 IU
- 香蕉96 IU
- 胡萝卜20,253 IU(这个真的是有点高了)
- 枣汁890 IU
- 鸡蛋279 IU
同样的将各自的贡献计数加总,满足每天摄入至少4000 IU,构成一个不等式
公式:
3) 钙
推导过程:
- 苹果9.6 mg
- 香蕉7 mg
- 胡萝卜19 mg
- 枣汁57 mg
- 鸡蛋22 mg
贡献计数加总,达到每日摄入最低1000 mg,构成不等式
公式:
4) 非负
推导过程:
基于常识,食物的数量不能为负,我们总不能凭空吐出一个苹果之类的,所以每种食物的数量也就是决策变量 必须非负,这样才能确保食谱设定是人类能操作的。由上,构成不等式
公式:
ALL
汇总一下我们就可以得到所有约束条件:
-
蛋白质:
-
维A:
-
钙:
-
非负:
3. 解决办法
本题出自《数学模型》(第六版)的4.1课后习题,由于书中已经有使用LINGO解决线性规划的示范,所以在这里我们介绍另外两种解法。
①MATLAB
在MATLAB中,一个linprog
函数可以用来求解线性规划问题。linprog
函数是求解最小化问题的,基本语法如下:
[x, fval] = linprog(f, A, b, Aeq, beq, lb, ub)
%目标函数系数向量,不等式约束 Ax ≤ b,等式约束 Aeq·x = beq,变量下界,上界
那么我们只需要往里面套系数,进行初始化矩阵即可:
f = [10; 15; 5; 60; 8]; % 目标函数系数(各食物价格)
A = [-0.3, -1.2, -0.7, -3.5, -5.5; % 蛋白质(取负)
-73, -96, -20253, -890, -279; % 维A(取负)
-9.6, -7, -19, -57, -22]; % 钙(取负)
%约束左端项,也就是不等式左边
b = [-50; -4000; -1000]; % 约束右端项,也就是不等式右边(取反)
lb = zeros(5, 1); % 非负约束
其中比较需要注意的一个点,就是取符号。因为linprog这个函数默认的解决形式是 Ax ≤ b,但是我们之前的所有约束条件都是≥的形式,所以我们要对整个不等式做一个取反,转换约束成 − Ax ≤ − b
初始化之后,就得调用线性规划的求解函数,也就是linprog
[x, fval, exitflag, output, lambda] = linprog(f, A, b, [], [], lb, [], options);
%输出函数 = linprog(输入函数)
此时如果我们直接打印五个决策变量,那就可以得到原来的价格情况下的最优解,也就是最合适的营养食谱。
那么针对题目这种更改条件的情况应该怎么解决呢?难道全部都要重新改代码吗?实则不然。
对(1)
第一小题更改的是需求,也就是右侧的约束值变化。而在前面初始化的时候,我们用到了lambda来记录约束的边际值,这是影子价格。在线性规划里头的影子价格(对偶变量)是约束条件的右侧值变化一个单位的时候,目标的函数值的变化量。因此我们只需要打印影子价格,并设定一个特判,就可以直接解决第一小问。
对(2)
第二小题更改的是价格,那就是目标函数的系数发生变化了。这个时候,我们就没法依靠对偶变量等其他方法进行延展求解,而是得修改对应的系数。我们看到小题中的变化只出现在一个系数(胡萝卜)上,工程量其实不大,所以我们不用担心,可以直接修改重新求解。
至于第二小问延展的两个分支:“改不改食谱”和“成本涨了多少”,我们可以对前后两个情况的最优食谱进行一次对比,如果最优解的向量基本相同,那么食谱就不用改变,然后总共的成本增长量则可以用两个成本值求差得到。
代码
前面的代码思路汇总一下,加上一些辅助的描述,就可以码出完整的解题代码:
f = [10; 15; 5; 60; 8];
A = [-0.3, -1.2, -0.7, -3.5, -5.5; -73, -96, -20253, -890, -279; -9.6, -7, -19, -57, -22];
b = [-50; -4000; -1000];
lb = zeros(5, 1);
options = optimoptions('linprog', 'Display', 'iter');
[x, fval, exitflag, output, lambda] = linprog(f, A, b, [], [], lb, [], options);
fprintf('最优解:\n');
fprintf('苹果数量: %.4f 个\n', x(1));
fprintf('香蕉数量: %.4f 个\n', x(2));
fprintf('胡萝卜数量: %.4f 个\n', x(3));
fprintf('枣汁数量: %.4f 杯\n', x(4));
fprintf('鸡蛋数量: %.4f 个\n', x(5));
fprintf('最小总费用: %.4f 角\n', fval);
% 影子价格
fprintf('\n影子价格:\n');
fprintf('蛋白质约束的影子价格: %.4f\n', -lambda.ineqlin(1));
fprintf('维生素A约束的影子价格: %.4f\n', -lambda.ineqlin(2));
fprintf('钙约束的影子价格: %.4f\n', -lambda.ineqlin(3));
% 问题(1)
fprintf('\n问题(1)分析:\n');
fprintf('维生素A需求增加1单位,费用增加 %.4f 角\n', -lambda.ineqlin(2));
fprintf('蛋白质需求增加1单位,费用增加 %.4f 角\n', -lambda.ineqlin(1));
fprintf('钙需求增加1单位,费用增加 %.4f 角\n', -lambda.ineqlin(3));
% 问题(2)
f_new = f;
f_new(3) = f(3) + 1; % 胡萝卜价格增加1角
[x_new, fval_new] = linprog(f_new, A, b, [], [], lb, []);
fprintf('\n问题(2)分析:\n');
fprintf('胡萝卜价格增加1角后:\n');
fprintf('新的最小总费用: %.4f 角\n', fval_new);
fprintf('费用增加: %.4f 角\n', fval_new - fval);
fprintf('新的最优解:\n');
fprintf('苹果数量: %.4f 个\n', x_new(1));
fprintf('香蕉数量: %.4f 个\n', x_new(2));
fprintf('胡萝卜数量: %.4f 个\n', x_new(3));
fprintf('枣汁数量: %.4f 杯\n', x_new(4));
fprintf('鸡蛋数量: %.4f 个\n', x_new(5));
if norm(x - x_new) < 1e-4 %特判
fprintf('食谱不需要改变\n');
else
fprintf('食谱需要改变\n');
end
尝试运行代码之后我们可以从日志里面看到,求解完成,效率可观,代码没有问题(csdn网站的代码运行有问题,如想确认建议直接cv到matlab中运行查看)
②Excel
1) 前导设置
由于Excel使用的“规划求解器”可能有些朋友没有设置,所以这里先做出一些引导:
Ⅰ点开左上角的 文件(File),在展开的页面选择左下角的选项(Options)
Ⅱ 选择 加载项,下方“管理(A)”下拉菜单选择 Excel 加载项(Excel Add-ins),点击 转到(G)
Ⅲ弹出的对话框中,勾选 规划求解器加载项点击确定,最后重启即可
2) 具体操作
Ⅰ建表
直接把题目的表格转换一下格式输入的即可:
Ⅱ调函数
在营养总摄入(G列)调用sumproduct函数,然后框选对应的营养标签行以及决策变量行:
示例如上,然后我们将全部三个营养标签和价格标签填补完全:
单元格 | 内容说明 | 公式示例 |
---|---|---|
G2 | 蛋白质总摄入 | =SUMPRODUCT(B2:F2, B7:F7) |
G3 | 维A总摄入 | =SUMPRODUCT(B3:F3, B7:F7) |
G4 | 钙总摄入 | =SUMPRODUCT(B4:F4, B7:F7) |
G5 | 总费用(目标函数) | =SUMPRODUCT(B5:F5, B7:F7) |
Ⅲ用工具
由于前面我们启用了规划求解器,所以我们直接点开数据那一栏,然后打开规划求解器:
选择总价格的格子(这里是G5)作为设置目标,选择几个决策变量作为通过更改可变单元格,根据我们之前推导的约束条件不等式填充到遵守约束当中,使无约束变量为非负数,最后选择求解方法为单纯线性规划,并求解
这样就算出原始情况下的最优方案了。
接下来的解决题目,我们有几种方法,可以用Excel的VBA求解,也可以直接人工手调:
对应需求 | 具体操作 |
---|---|
维A需求+1 IU | 改 G3 ,需求值调到4001,重运 |
蛋白质需求+1 g | 改 G2 的需求值跳到51,重运 |
钙需求+1 mg | 改 G4 的需求值跳到1001,重运 |
胡萝卜+1角 | 改 D5 由5到6,重新运行然后和原始情况的结果比对 |
4. 答案结果
由于两个的答案都是一样的,所以我们在这里就只分析第一种方法的了。运行代码之后,我们可以得到结果:
得到初始的最优食谱:约 49.4 个胡萝卜,约 2.8 个鸡蛋,其他全不要,最小总费用约为 269.36 角
-
第一小题
- 维A影子价格为 0,表示增加需求不会导致食谱变化。在目前的最优解下,维A的摄入量已经超过了最低需求,少量需求的变化只是小打小闹,不会改变食谱,也不会增加成本。
- 蛋白质的影子价格约 0.4714 角,也就是蛋白质需求增加1克,费用成本将增加约 0.4714 角。
- 钙影子价格约为 0.2458 角,即钙需求增加1毫克,费用成本将增加约0.2458 角。
-
第二小题:
胡萝卜价格增加1角后最优食谱构成不变(仍然是约49.4个胡萝卜和约2.8个鸡蛋),总成本增加了约49.38角 (增加的单位成本 * 胡萝卜数量 )