吃拼好饭可能会不健康,那如果是自己做呢?倘若我们要自己定制一套健康餐,那么成本方面的考虑呢?假设今天阿坤要在兼顾健康和自己的预算的情况下,尝试规划一套合适的健康套餐,规划的方法有很多种,层出不穷,不胜枚举,它希望找一种轻松省心的方式进行规划......
一. 题目
某学校为学生提供营养套餐,希望以最小费用来满足学生对基本营养的需求.按照营养学家的建议,一个人一天对蛋白质、维生素 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解决线性规划的示范,所以在这里我们介绍另外两种解法。
①程序
I MATLAB
在MATLAB中,一个linprog函数可以用来求解线性规划问题。linprog函数是求解最小化问题的,基本语法如下:
[x, fval] = linprog(f, A, b, Aeq, beq, lb, ub)
%目标函数系数向量,不等式约束 Ax ≤ b,等式约束 Aeq·x = beq,变量下界,上界
其中:
是目标函数的系数向量。这里的线性规划函数的目标是最小化
(默认求的最小化),这里的
是一个列向量,代表各个变量对应的系数。就比方说,如果目标函数是
,那么就是写成
f = [0.3; 1.2]
A和b是不等式的约束矩阵和向量,即约束条件 ,
A是矩阵,每行代表一个不等式,b是对应的不等式右端项。
然后Aeq和beq则是等式约束矩阵和向量。即约束条件(只有这种形式),用在要满足的线性等式上。举个简单例子,如果我们的约束条件是
,那么我们就得写成
的形式,这样以来这个等式约束才被传入linprog中作为必须严格满足的条件。
对lb和ub,可能学过cpp二分函数的朋友就会比较熟悉了,lb对应的是lower bound,ub对应的upper bound,两者自然而然就是变量的下界和上界,分别表示各个变量的最小值和最大值(也可以设置成-Inf或Inf,表示成无限制)。例如 lb = [0; 0] 表示所有变量都不能为负。这是非负约束对应部分的参数名。
左边的是输出变量,也就是求解后所有决策变量的最优值。
fval是输出变量,即目标函数在最优解处的值,换言之就是最小目标函数值。
总的来说,就是设置了 来求出满足这些约束条件的变量
,让目标函数值最小。
了解完基本语法还有对应规则之后,我们只需要往里面套系数,进行初始化矩阵即可:
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(输入函数)
这里比起之前给的函数展示,多了三个输出变量,这里解释一下:
exitflag:判断是否成功找到了最优解,还有求解状态,一般取值1的时候就是有最优解,取0就是到了最大的迭代次数了还是没找到最优解,小于零就是无解或者报错output:给出求解过程的信息,比方说迭代次数,使用时间,还有收敛的信息之类的lambda:对偶变量的结构体,包含影子价格(约束条件中资源“多给”或“少给”一单位,目标函数比如总成本如何变化)这些信息
此时如果我们直接打印五个决策变量,那就可以得到原来的价格情况下的最优解,也就是最合适的营养食谱。
那么针对题目这种更改条件的情况应该怎么解决呢?难道全部都要重新改代码吗?实则不然。
对(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中运行查看)
II Python
由于matlab部分的叙述已经差不多了,所以python这里我们就一笔带过,直接用scipy.optimize.linprog 库解决即可。
代码
import numpy as np
from scipy.optimize import linprog
c = np.array([10, 15, 5, 60, 8])
A_ub = np.array([
[-0.3, -1.2, -0.7, -3.5, -5.5], # 蛋白质
[-73, -96, -20253, -890, -279], # 维生素A
[-9.6, -7, -19, -57, -22] # 钙
])
b_ub = np.array([-50, -4000, -1000])
bounds = [(0, None), (0, None), (0, None), (0, None), (0, None)]
# --- 问题1 ---
print("--- 问题1 ---")
result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs')
# 检查
if result.success:
x = result.x
fval = result.fun
print('最优解:')
print(f'苹果数量: {x[0]:.4f} 个')
print(f'香蕉数量: {x[1]:.4f} 个')
print(f'胡萝卜数量: {x[2]:.4f} 个')
print(f'枣汁数量: {x[3]:.4f} 杯')
print(f'鸡蛋数量: {x[4]:.4f} 个')
print(f'最小总费用: {fval:.4f} 角')
shadow_prices = result.ineqlin.marginals #这里的调用需要看scipy的版本,不同的版本是不一样的
print('\n影子价格:')
print(f'蛋白质约束的影子价格: {shadow_prices[0]:.4f}')
print(f'维生素A约束的影子价格: {shadow_prices[1]:.4f}')
print(f'钙约束的影子价格: {shadow_prices[2]:.4f}')
# --- 敏感性分析 ---
print('\n问题1的分析:')
print(f'维生素A需求增加1单位,费用增加 {shadow_prices[1]:.4f} 角')
print(f'蛋白质需求增加1单位,费用增加 {shadow_prices[0]:.4f} 角')
print(f'钙需求增加1单位,费用增加 {shadow_prices[2]:.4f} 角')
# --- 问题2:胡萝卜 ---
print('\n--- 问题2 ---')
c_new = np.copy(c)
c_new[2] = c[2] + 1
result_new = linprog(c_new, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs')
if result_new.success:
x_new = result_new.x
fval_new = result_new.fun
print('胡萝卜价格增加1角后:')
print(f'新的最小总费用: {fval_new:.4f} 角')
print(f'费用增加: {fval_new - fval:.4f} 角')
print('新的最优解:')
print(f'苹果数量: {x_new[0]:.4f} 个')
print(f'香蕉数量: {x_new[1]:.4f} 个')
print(f'胡萝卜数量: {x_new[2]:.4f} 个')
print(f'枣汁数量: {x_new[3]:.4f} 杯')
print(f'鸡蛋数量: {x_new[4]:.4f} 个')
# 比较新旧食谱
if np.linalg.norm(x - x_new) < 1e-4:# 设置个阈值
print('食谱不需要改变')
else:
print('食谱需要改变')
else:
print("问题(2)求解失败:", result_new.message)
else:
print("原始问题求解失败:", result.message)
比较需要注意的一个点:
scipy.optimize.linprog函数在不同版本之间,其返回的OptimizeResult对象的结构可能会略有差异,可能有时候还是可以出结果的,但是也会有报错。为了解决这个问题,我们可以利用dir()和print()就拿我的环境举例:
import numpy as np from scipy.optimize import linprog c = np.array([10, 15, 5, 60, 8]) A_ub = np.array([ [-0.3, -1.2, -0.7, -3.5, -5.5], [-73, -96, -20253, -890, -279], [-9.6, -7, -19, -57, -22] ]) b_ub = np.array([-50, -4000, -1000]) bounds = [(0, None), (0, None), (0, None), (0, None), (0, None)] print("检查:") result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs') if result.success: print("\n result 对象的所有可用属性 (dir(result))") print(dir(result)) # 打印对象的所有属性名 print("\n result 对象的详细内容 (print(result))") print(result) # 打印对象的详细字的符串表示 else: print("\n求解失败,无法打印result。错误:", result.message) print("\n 结束")
dir(result)会返回一个字符串列表,里面包含了result对象所有可以直接访问的属性和方法的名称。我的结果大概是这样的:
可以看到了
fun(目标函数值),x(最优解),success(是否成功) 等相对熟悉的名字,还有ineqlin这个词,这个指不等式约束,和影子价格强相关,所以可以初步判断影子价格就在ineqlin属性中。接着就打印出了
result对象的内容摘要,此时从中找到初步判断时的ineqlin:这一部分,在ineqlin下面,我们可以看到residual:和marginals,而marginals这个词直翻就是“边际的”,一眼定真,marginals就是最终对应的影子价格,它的值也就是三个约束对应的对偶变量。综上,结合
dir(result)我们就可以知道有ineqlin这个属性,然后用print(result)就可以知道ineqlin内部有一个叫做marginals的属性存储了我们所需的影子价格。所以也就可以敲定访问影子价格的、符合相应(我的)版本的路径:shadow_prices = result.ineqlin.marginals当然,最最高效也是最建议的方法,就是直接把用上面的测试代码的结果直接cv给ai,让ai识别一下然后改代码,最后cv回到python编译器。
②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. 答案结果
由于两个的答案都是一样的,所以我们在这里就只分析第一种方法的了。运行代码之后,我们可以得到结果:
①MATLAB的

②Python的

得到初始的最优食谱:约 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角 (增加的单位成本 * 胡萝卜数量 )
补充:
1.参考来源
[1] 姜启源. 数学建模(第六版)[M]. 北京: 高等教育出版社, 2024: 103.
2.一些后话
我们知道了答案的食谱大概是49.4个胡萝卜和大约2.8个鸡蛋,但是我们也需要知道,数学建模是一个将现实情景转化成数学语言,然后计算得到最优或者是较优结果的过程,它最终服务的是现实,也就是说我们的这个结果是需要的映射抑或能实践到现实的。而我们发现,目前用线性规划得到的结果是带小数的,可是在现实之中,如果我们要去市场买菜,总不可能只买0.4个胡萝卜,一般也不可能买到0.8个鸡蛋,这些都是不现实的。
那么我们应该怎么操作呢?
在单纯线性规划的方法下,我们可以往两边取整,也就是向上或者是向下取整,然后对比向上向下两种情况的最终结果,进而取最优方案。
但是有些时候数据是离散的,没办法直接上下取整得到最优值,有可能最优值并不是直接取整得到的那个,所以我们需要引入新的一种方法,那便是整数规划。相较于一般的线性规划,整数规划的决策变量必须得是整数,因此也就不会出现有小数的答案了。这也是我们下一篇的内容。
线性规划就暂且告一段落了,感谢各位阅读。
THE END

可以看到了
378

被折叠的 条评论
为什么被折叠?



