简介面向数学建模竞赛参赛者的赛题解析资料聚焦2024年电工杯B题围绕大学生一日食谱的膳食营养评价与优化问题完整讲解从问题理解、模型建立到程序实现的全流程。压缩包内含一个docx文档体积约三十二千字节虽小却系统梳理了赛题分析、计算公式与代码实现。文档先依据参考标准计算能量、蛋白质、脂肪、碳水化合物及维生素矿物质摄入量评估膳食结构与营养素缺乏或过量情况再针对高校食堂食材提出增加优质蛋白与蔬果、减少高脂高糖食物的调整方案最后构建营养优化模型用编程求解并生成平衡膳食的日食谱与周食谱表格。读者可对照样例代码快速复现求解过程掌握数据处理与建模思路适合备赛同类竞赛或膳食优化建模任务的学生参考。已有892人学习下载可作为备赛阶段的直接借鉴。1. 电工杯B题到底考什么不只是算法是整套建模流程很多队伍在电工杯B题上翻车不是败给算法不够新而是败给流程拿到题目花一天半读数据建模到第三天发现约束漏了最后一天通宵改代码提交前连结果合理性都没验证。B题和A题最大的差别在于它往往给足真实场景和表格数据工程量集中在数据清洗、约束梳理和结果解释上而不是拼谁会用神经网络。你需要的不是某个灵光一现的模型而是一条能稳定复现的流水线读题拆问题、洗数据、建模型、写代码求解、排错、验结果。这篇笔记就按这个顺序把完整过程拆开讲代码直接能跑参数掰开揉碎说清楚适合正在备赛的学生也适合第一次带队的指导者照着检查进度。2. 把题意翻译成数学模型目标函数与约束条件的拆解2.1 第一步不是写代码是给问题下定义我见过太多队伍对着赛题第一反应就是打开编辑器结果写了一堆用不上的函数。正确做法是用半天时间把题目文字翻译成数学语言翻译完了代码只是体力活。B题的题干通常包含场景描述、数据表说明、求解要求三部分你要从中提取三类信息决策变量、目标函数、约束条件。这三样缺一个模型就没法定型。具体操作建议按这个顺序走先把题目里所有“求”“使”“最小化”“最大化”字样的句子圈出来那里面藏着目标函数再把所有“满足”“受限于”“不超过”“不低于”“必须”圈出来那里面藏着约束最后看表格的列名和数据范围确定哪些量是给定的参数、哪些量是要算的决策变量。这一步做完你会得到一张草稿纸上面是数量关系而不是中文句子。2.2 决策变量三分类与建模表格决策变量在电工杯B题里通常分三类连续变量、整数变量、0-1变量。连续变量比如发电机出力、流量大小整数变量比如设备台数、人员数量0-1变量最常见比如设备是否启动、开关是否闭合、方案是否采用。三类变量决定了后面求解器的选型这一步分错了后面代码结构全错。为了不容易漏我一般会画一张建模表把题目给的每一个物理量填进去。下面这张表是通用模板你可以直接抄物理量符号类型范围/取值来源机组出力P_i(t)连续[P_min, P_max]待决策机组启停x_i(t)0-10或1待决策负荷需求D(t)参数给定序列数据表燃料成本系数c_i参数给定数据表最大爬坡率r_i参数给定数据表表格填完你就能直观看出哪些量是求解器要算的、哪些是读数据进来的。一个重要的检查点如果题目给了一张数据表表里每个字段都必须能在你的建模表里找到归宿找不到的那个字段要么是冗余信息要么是你漏读了一个约束后者的概率远大于前者。2.3 约束条件的层次化整理约束条件不要一股脑写进代码先按层次理清楚。我习惯分四层第一层是物理边界也就是变量的上下界比如出力不能超过机组额定功率第二层是平衡约束比如每个时段总出力等于总负荷第三层是逻辑约束比如设备启动了才能出力也就是出力大于0的前提是状态变量为1第四层是工程附加约束比如机组不能频繁启停、检修时段不能运行。这四层优先级依次降低前三层是模型正确性的底线第四层是得分点。一个容易犯的错误是把所有约束写成一大段不等式不分层。后果是当求解器报“无解”的时候你根本不知道是哪一层约束在打架。正确的做法是在纸上先把约束写成带编号的数学表达式比如“约束C1∑P_i(t) D(t), ∀t”然后再开始写代码。一旦后期排查你能按编号快速禁用某组约束做测试而不是对着几百行代码发呆。这一步花两小时后面能给你省两天。3. 数据预处理与特征构造从赛题数据到可求解的输入3.1 读取赛题数据格式假设与读取代码电工杯B题的数据文件常见格式是Excel表格偶尔也会给CSV。第一个坑就是打开文件的瞬间发现表头不是第一行、列名带单位、时间列是文本格式。这些问题不处理后面的合并和计算全崩。下面这段代码是我每次拿到赛题数据都会先跑一遍的“体检脚本”import pandas as pd import numpy as np # 读取Excel假设sheet名为Sheet1 df pd.read_excel(load_data.xlsx, sheet_nameSheet1) # 体检查看行列数、列名、每列非空数、数据类型 print(形状:, df.shape) print(列名:, df.columns.tolist()) print(类型:\n, df.dtypes) print(缺失统计:\n, df.isnull().sum()) print(前5行:\n, df.head())这段代码的作用是让你的数据底牌暴露出来而不是靠肉眼猜。注意四点第一read_excel的sheet_name参数要和你实际打开Excel时看到的表名一致有时候表名叫“数据1”而不叫Sheet1读不对就报错第二df.dtypes会告诉你哪些列被读成了字符串比如时间列如果显示object说明需要转时间格式第三isnull().sum()是排查缺失值最快的方式比手动翻Excel靠谱得多第四head()必须看因为表头有没有被当数据读进去看前两行就知道。3.2 缺失值与异常值处理为什么不能用均值填赛题数据里常见两类问题一是某些时段的数据缺失二是某些采样点数值明显偏离物理范围比如负荷突然变成负数或超出变压器容量。很多队伍图省事直接用均值填充这是个隐蔽的坑。对于电力负荷这类强时间相关序列均值填充会直接抹掉峰谷特征导致后面约束被错误满足或违反。我一般这样处理先看缺失值比例少于总样本量5%的用前后时刻线性插值比率较高的先检查是不是整个时间段缺失——那可能是题设故意让你合并外部数据而不是让你猜。对于异常值用物理边界判断超出设备容量的直接标为异常然后按上下相邻点插值修正。代码里用线性插值最稳妥# 时间列转datetime df[time] pd.to_datetime(df[time]) # 按时间排序务必先排序再插值 df df.sort_values(time) # 设定数值列的物理边界示例负荷列合理范围为[0, 2000] col load df.loc[df[col] 0, col] np.nan df.loc[df[col] 2000, col] np.nan # 线性插值填充缺失值 df[col] df[col].interpolate(methodlinear, limit_directionboth)这里有个细节值得说interpolate默认不填充序列开头和结尾的缺失值所以加了limit_directionboth让首尾也能用最近邻的逻辑补齐。注意先sort_values再插值是必须的否则时间乱序时插值结果没有物理意义。做完这一步把处理后的数据画个折线图肉眼看一遍有没有尖刺或平台数值问题基本就能拦住。3.3 特征构造与量纲统一B题的数据往往不是直接可用需要构造派生特征。最常见的几个峰谷时段标识把24小时分成峰、平、谷三段、累计平均值、滑动窗口均值、或者把日期转换成星期几。这些特征的作用是让模型捕捉到负荷的周期性规律或者让优化约束变得更贴近实际。比如分时电价场景峰谷标识就是成本计算的关键输入。量纲统一是这里最容易翻车的地方。题目给你的数据可能是功率单位千瓦、成本单位元每兆瓦时、时间单位小时混在一起的时候必须全部折算到同一套单位制。我见过一组队伍在目标函数里加了燃料成本和启停成本一个单位是元、另一个是万元结果求解器给出的方案看起来“成本极低”其实是把不同量纲的数直接相加了。建议在代码开头定义一个基础单位比如功率统一用兆瓦、成本统一用元、时间统一用小时所有数据进来先换算再进模型。这个习惯能避免90%的数值玄学问题。4. 核心求解代码基于MILP的完整实现与参数解读4.1 求解器选型什么时候用线性、什么时候用混合整数到了求解环节先不要急着套遗传算法。B题里绝大多数优化问题都可以用线性规划LP或混合整数线性规划MILP解决区别在于你的决策变量里有没有整数或0-1变量。全连续变量就用scipy.optimize.linprog含0-1或整数变量就用scipy.optimize.milp这两个都是开箱即用的求解器不需要额外许可证。很多队伍一看到“优化”两个字就上遗传算法理由是怕模型是NP难的。实际上B题给的规模通常不会大到求解器跑不动。我做过一次对比一个包含30台设备、24个时段的MILP模型scipy.optimize.milp默认参数下几秒到几十秒就能出结果而同样的问题用遗传算法参数调不好时可能陷入局部最优还解释不清。能用精确求解器解决的就坚决不用启发式。等精确求解器确实跑不动了再考虑上启发式。4.2 完整求解代码机组启停与负荷分配下面这套代码是一个典型的机组组合Unit Commitment简化模型目标是最小化燃料成本和启动成本约束包括负荷平衡、出力上下限、启停逻辑。这是电工杯B题里出镜率最高的优化骨架你拿着换成赛题里的设备和约束就能用。注意这里我特意用小型示例数据保证代码可以直接运行import numpy as np from scipy.optimize import milp, LinearConstraint, Bounds # ---------- 参数设置 ---------- n_units 3 # 机组数量 T 24 # 时段数单位小时 Pmin np.array([50, 30, 20]) # 各机组最小出力MW Pmax np.array([200, 150, 100]) # 各机组最大出力MW fuel_cost np.array([0.55, 0.65, 0.75]) # 燃料成本元/MWh start_cost np.array([300, 200, 150]) # 启停成本元/次 # 24小时负荷需求单位为MW实际赛题从Excel读入 demand np.array([ 180, 170, 160, 155, 150, 160, 200, 240, 280, 320, 330, 310, 290, 280, 300, 340, 360, 380, 420, 400, 360, 300, 220, 190 ]) # ---------- 变量编号约定 ---------- # 变量排列顺序前 n*T 个是启停状态 x中间 n*T 个是出力 p最后 n*T 个是启动辅助变量 s def idx_x(i, t): return i * T t def idx_p(i, t): return n_units * T i * T t def idx_s(i, t): return 2 * n_units * T i * T t n_var 3 * n_units * T # ---------- 目标函数系数 ---------- c np.zeros(n_var) for i in range(n_units): for t in range(T): c[idx_p(i, t)] fuel_cost[i] # 出力成本 c[idx_s(i, t)] start_cost[i] # 启动成本 # ---------- 变量边界 ---------- lb np.zeros(n_var) ub np.ones(n_var) for i in range(n_units): for t in range(T): lb[idx_p(i, t)] Pmin[i] ub[idx_p(i, t)] Pmax[i] ub[idx_s(i, t)] 1.0 bounds Bounds(lb, ub) # ---------- 约束矩阵组装 ---------- A_rows [] lb_con [] ub_con [] # 约束1每个时段总出力等于负荷需求 for t in range(T): row np.zeros(n_var) for i in range(n_units): row[idx_p(i, t)] 1 A_rows.append(row) lb_con.append(demand[t]) ub_con.append(demand[t]) # 约束2出力受启停状态约束p 在 0 和 x*Pmax 之间 for i in range(n_units): for t in range(T): row1 np.zeros(n_var) row1[idx_p(i, t)] 1 row1[idx_x(i, t)] -Pmax[i] A_rows.append(row1) lb_con.append(-np.inf) ub_con.append(0) # 保证 p - x*Pmax 0 row2 np.zeros(n_var) row2[idx_p(i, t)] -1 row2[idx_x(i, t)] Pmin[i] A_rows.append(row2) lb_con.append(-np.inf) ub_con.append(0) # 保证 -p x*Pmin 0 # 约束3启动变量与状态变化的关系 s x[t] - x[t-1] for i in range(n_units): for t in range(1, T): row np.zeros(n_var) row[idx_x(i, t)] 1 row[idx_x(i, t - 1)] -1 row[idx_s(i, t)] -1 A_rows.append(row) lb_con.append(-np.inf) ub_con.append(0) # 保证 x[t] - x[t-1] - s 0 constraints LinearConstraint(np.array(A_rows), lb_con, ub_con) # ---------- 指定整数变量启停状态为0-1 ---------- integrality np.zeros(n_var) for i in range(n_units): for t in range(T): integrality[idx_x(i, t)] 1 # 1表示整数约束 # ---------- 求解 ---------- res milp(c, integralityintegrality, boundsbounds, constraintsconstraints) # ---------- 结果输出 ---------- if res.success: print(求解成功总成本: %.2f 元 % res.fun) x_opt res.x[:n_units * T].reshape(n_units, T) p_opt res.x[n_units * T:2 * n_units * T].reshape(n_units, T) print(各时段总出力:, p_opt.sum(axis0).round(1)) print(机组1启停状态:, x_opt[0].astype(int)) else: print(求解失败:, res.message)这段代码的核心逻辑分成四个模块变量定义、目标函数、约束组装、求解输出。milp函数的四个关键参数分别是c目标系数向量、integrality标记哪些变量是整数、bounds变量的上下界、constraints线性约束。特别注意integrality里我只把启停状态x标记为1出力p和辅助变量s保持0也就是连续变量——如果你把p也标记成整数模型会被迫要求出力是整数兆瓦那既不符合物理实际也会让求解变慢几倍。LinearConstraint的组装方式是用三个数组描述一组不等式A_rows是系数矩阵的每一行lb_con和ub_con是每行的下界和上界。对于形如A·x ≤ b的不等式下界设成-np.inf、上界设成0对于等式约束上下界都设成同一个数。这个写法是scipy的风格和pulp那种逐条addConstraint不同习惯后批量组装更快也不容易漏约束。4.3 结果的工程检验可行性与可解释性求解器返回successTrue不代表结果一定正确。我拿到结果习惯先做三个检验第一把每个时段的p_opt.sum(axis0)和demand逐项对比误差超过1e-6就说明约束组装时漏了某个时段第二检查启停状态和出力是否匹配也就是某个机组x0的时段对应出力必须为0这是约束2在起作用第三把目标函数值拆开分别统计燃料成本和启动成本看看启动成本占比是否合理——如果启动次数多得离谱通常说明你漏掉了最小启停时间约束误差会被评委一眼看出来。另外res.fun的单位取决于你目标系数c的单位。前面写的fuel_cost单位是元每兆瓦时出力单位是兆瓦时间单位是小时所以乘积单位就是元。这个换算链要在论文里写清楚很多评委专门挑这种单位口径不统一的错。5. 避坑清单五天赛程里最常翻车的五个环节5.1 单位错位数字对了但物理意义错了现象求解结果的总成本数值很小小到明显不符合常识或者约束“看似满足”但画图上曲线对不上。原因数据表里某列的单位是千瓦模型里却按兆瓦参与运算导致数量级差了1000倍。解决在代码开头写一个单位换算区所有数据读入后第一时间统一到一套单位制再进入模型。输出结果时再换回题目要求的单位不要在模型内部来回切换单位。5.2 漏掉隐式约束边界条件拆题不干净现象模型跑通了结果也很漂亮但对照赛题原文发现有个前提条件没有被体现比如“机组在检修时段不得启动”“设备启动后至少运行2小时”。原因拆题时只看到了显式的数字约束忽略了文字描述里的逻辑约束。解决把题目文字逐句过一遍遇到描述运行规则的长句全部改写成逻辑表达式写不进数学表达式的也要在论文里作为假设说明。宁可多设一个约束让问题变紧也不能漏掉一个约束扣分。5.3 求解器提示infeasible不是题目错了是建模漏了现象milp返回successFalse报错信息显示无可行解。原因最常见的三个——负荷数据有误导致平衡约束达不到某个机组Pmin设得过高在低谷时段无法只靠这一台机组满足低负荷约束之间互相矛盾比如同时要求某机组“必须运行”和“出力为零”。解决用二分法排查先把第四层附加约束全删掉只保留平衡约束和物理边界跑通了再加回一条、再跑直到找出哪两条约束打架。注意Pmin和Pmax的赋值不能搞反这个错误我见过不止一次。5.4 时间戳没有对齐跨天数据排错的常见坑现象预处理后画图负荷曲线在每天交界处出现断崖式跳变或者某些时段的出力计算出来是负数。原因原始数据的时间列是字符串格式排序时按字母序排导致“2024-06-01 08:00:00”排在“2024-06-01 07:00:00”前面整个序列错位。解决读入时间列后立即执行pd.to_datetime然后强制sort_values排序后再检查首尾时间是否连续。这个坑特别隐蔽因为大部分数据看起来还是“有序”的只有画图才能暴露。5.5 可视化误导自己给自己造成困惑现象画出的调度甘特图负荷曲线看着完美但团队内部对结果理解不一致有人觉得“曲线光滑过头了不真实”。原因可视化代码里用了平滑插值或默认的线条填充掩盖了实际离散采样点的信息或者坐标轴范围设得不合理把异常波动压平了。解决画结果图一律用散点加折线不额外平滑坐标轴刻度和范围固定不要用自动缩放给每张图标注单位。可视化是为了让评委快速抓住你的方法而不是制造解释不清的视觉误差。6. 验证与进阶灵敏度分析让模型站得住脚B题拿奖和拿高分的分界线往往在于有没有做验证。最容易被忽略但性价比最高的验证手段是灵敏度分析把关键参数从基准值上下浮动10%~20%重新求解观察目标函数值的变化方向和幅度。这一步直接回应了评委最爱问的“你这个结果对参数敏感吗”。代码很简单基于第4章的模型把demand整体放大1.1倍再跑一次milp对比前后总成本。我习惯做成参数扫描记录每个场景下的目标值和决策变量整理成一张表scale_list [0.8, 0.9, 1.0, 1.1, 1.2] cost_list [] for scale in scale_list: demand_scaled demand * scale # 重新拼约束矩阵此处省略与第4章相同的装配代码 # res milp(...) cost_list.append(res.fun) print(不同负荷水平下的最优成本:) for s, c in zip(scale_list, cost_list): print(f 负荷倍数 {s:.1f} - 成本 {c:.2f} 元)如果成本随负荷近似线性增长说明模型行为符合物理直觉如果某一段出现跳跃大概率是某个机组在临界点启停这个现象反而值得写进论文作为结论的支撑。除了灵敏度分析我还有个多年养成的习惯深夜写好代码第二天早上重新读一遍自己的约束清单再提交因为熬夜时最容易犯“以为写了但实际没写”的低级错误。这不是客套话是真实成本换来的习惯。希望这篇拆解能帮你在赛程里少走几个弯路把时间花在真正拉分的地方。本文还有配套的精品资源点击获取