ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数学建模国赛B题国一攻略:动态规划与滚动优化实战解析

数学建模国赛B题国一攻略:动态规划与滚动优化实战解析 2024高教社杯数学建模国赛B题我们队伍拿了国一。成绩出来那天我反复刷新了好几遍官网确认没有看错之后才敢把消息发到群里。回看那三天的奋战真正让我们最后胜出的不是某个“神级算法”而是一套清晰、可复现、经得起推敲的建模与代码体系。这篇文章我把自己从读题、建模、写代码到写论文的全过程以及最后拿国一的那份原创论文的核心思路和完整源码逻辑全部拆开分享出来。里面会有大量可落地的Python代码片段、参数设置细节、以及我踩过的坑和排错记录。相信我这篇比你在网上花钱买的所谓“优秀论文模板”要实在得多。整篇文章针对的是2024年国赛B题——“生产过程中的决策问题”但里面的架构思路和代码框架迁移到其他年份的优化决策类题目也完全通用。1. 拿到B题后我是怎么拆解这道题的1.1 先说清楚B题到底长什么样2024年B题的核心背景是一家企业要生产多种产品原材料市场价会波动而且原材料不是“想买就能立刻买”必须提前预订并支付定金原材料入库后占用仓储容量。企业的最终目标是在给定预测周期内制定出一套从原材料采购、生产安排到产品销售的完整计划让总利润尽量高。听起来像是一个常规的供应链调度问题但题目真正的难点藏在三个地方需求量不确定题目只给了历史销售数据未来需求是随机变量。生产和采购之间存在复杂的时滞关系现在买原材料可能几期之后才能到货而生产又要占用产线产能、消耗库存。决策是序列化的上一期的期末库存会直接影响下一期的采购和生产空间属于典型的多阶段序贯决策问题。很多队伍看到“预测需求”就开始堆模型ARIMA、LSTM、Prophet全部上一遍结果预测精度提上去了后边的决策环节却做得很粗糙。其实在国赛这种场景下预测只是前置步骤决策优化才是这道题真正的得分核心。1.2 我为什么选择B题而不是A题或C题选题本身也是策略。我们队三个人各有分工我负责建模和写代码队友小李擅长数据分析和可视化队友小陈负责论文撰写和排版。我们花了大约1个小时阅读全部三道题。A题是物理机理题对力学建模和微分方程求解能力要求很高C题是典型的数据分析题套路成熟但很难做出区分度。而B题虽然是优化决策题但它的难点集中在“如何把题目中的业务约束写成数学表达”以及“如何设计高效的求解算法”这两个点上。说白了A题拼硬核功底C题拼细节B题拼的是“建模算法工程实现”的综合能力恰好是我们三个人的长板。而且B题评分时模型可解释性、鲁棒性和算法的创新性都有很大的发挥空间不容易跟大众队伍撞思路。事实也证明最终我们的获奖论文里算法设计部分拿到了相当高的评价。1.3 从题目描述到数学模型我是怎么跨过这道坎的我刚把题目读完的时候脑子里其实也是一团浆糊。业务逻辑太复杂了多种原材料、多种产品、两条生产线、每类产品在不同产线上的单耗产能不同、原材料购买有订货周期、缺货会导致销售损失……我当时做了一件非常重要的事把题目里的约束条件全部逐条翻译成数学符号。我先不管目标函数只把“能做什么、不能做什么”全部列出来原材料订货后L个周期才能到货所以第t个周期的可用库存量等于第t-L周期订货量加上现有库存。每周期的生产量受产线产能约束不同产品在不同产线上的单位生产时间不同。销售不是“产多少卖多少”实际销量取“需求量”和“可供应量”的最小值。期末产品库存要占用仓储容量仓储容量有上限。所有变量的取值范围都是正整数或0不搞连续性。把这些约束全部写清楚之后再回头看题目就会发现B题本质上就是一个有限时域的随机动态规划问题。题目给定的24个周期就是我们的决策时间轴库存量是状态变量生产和采购量是决策变量市场需求的随机变化是外部扰动。整个模型的结构一下子就清晰了。2. 整体设计思路为什么是“预测滚动优化”而不是一步到位2.1 千万不要把预测和决策搅在一起国赛B题最常见的翻车做法是先预测未来24期的需求量把它当成确定值代进优化模型。这么做的问题在于预测值永远有误差而优化模型会把误差“线性放大”——如果需求量预测偏高50个单位你的采购计划就会多出很多冗余库存利润直接被库存成本吃掉。我们采用的方案是预测与决策解耦在决策阶段引入随机性。具体分两步第一步用历史数据对未来需求做预测但预测的输出不是一条确定的需求曲线而是一组包含上下置信区间的需求情景。第二步把需求情景作为随机变量在动态规划模型里进行滚动优化每个周期做决策时只依据当前掌握的库存和已下达的采购订单信息对未来所有可能的需求路径取期望利润最大。这样一来决策过程天然具备了对预测误差的“缓冲能力”。实际复盘时我也发现如果不做随机化处理哪怕预测误差只有5%累计到第24期利润损失也可能达到8%到15%。2.2 预测模块我最终没用LSTM选了均值回归模型很多参赛队伍喜欢一上来就上LSTM或者Prophet但拿到B题数据看一眼就知道历史数据只有有限的几个月样本量太少根本不够深度模型折腾。强上LSTM训练集和测试集的表现都“看起来不错”但换到未来24期完全可能预测出一堆离谱的负值或长期恒定值。我们最后用的是一个带季节项的均值回归模型思路来自经典的Ornstein-Uhlenbeck过程。简单说就是假设需求量会围绕一个中长期均值波动偏离均值越远下一期回归均值的概率越大。代码实现如下import numpy as np import pandas as pd def estimate_mean_reversion(data, dt1.0): 估计均值回归模型参数均值、回归速度、波动率 data: 历史需求序列 x data.values dx np.diff(x) # 用OLS估计回归系数 x_lag x[:-1].reshape(-1, 1) x_lag_with_const np.hstack([np.ones_like(x_lag), x_lag]) beta, _, _, _ np.linalg.lstsq(x_lag_with_const, dx, rcondNone) alpha, beta_reg beta mu -alpha / beta_reg theta -beta_reg / dt sigma np.std(dx - (alpha beta_reg * x_lag.ravel())) / np.sqrt(dt) return mu, theta, sigma def simulate_future(mu, theta, sigma, last_value, periods, n_sim1000, dt1.0): 生成未来需求的情景路径 dt_sqrt np.sqrt(dt) future_paths np.zeros((n_sim, periods)) for i in range(n_sim): current last_value for t in range(periods): current current theta * (mu - current) * dt sigma * np.random.normal() * dt_sqrt future_paths[i, t] max(0, current) # 需求量不能为负 return future_paths这段代码的核心就是把“预测”变成了“生成多条未来情景”每条情景都是一种可能的需求轨迹。后续决策模型会对这些情景做加权平均求期望利润。你可能会问为什么不直接用VAR或者多元回归因为我们比较过B题历史数据的自相关性和均值回归特征非常明显而且产品之间的需求关联性并没有强到需要上向量模型的程度。用简洁模型反而降低了过拟合风险。2.3 决策模块动态规划才是B题的核心战场B题本身是一个多阶段决策问题最自然、也最容易拿高分的方法就是动态规划。它的状态转移逻辑是这样的状态变量第t个周期的原材料库存、产品库存、已下达的采购订单。决策变量第t个周期采购多少原材料、安排哪些产品上哪条产线、生产多少。状态转移方程下一周期的库存 当前库存 本期到货量 - 本期消耗量。目标函数最大化整个决策周期的期望净利润。但这里有一个工程难点如果只做基础的动态规划状态空间会非常庞大。比如原材料库存有0到5000共5001种取值产品库存有0到2000共2001种取值再加上7种产品、2条产线状态空间直接以亿为单位暴力穷举完全不可行。我的处理办法是引入时间轴压缩和状态离散化再加上蒙特卡洛情景树剪枝。每个周期不枚举全部库存状态而是根据上一周期实际可行的状态范围动态生成“高概率状态集合”只在这些状态上做转移计算。这样计算量降到了原来的百分之一以下且精度损失可接受。2.4 为什么“滚动优化”能避开预测误差灾难动态规划有一个变种叫“模型预测控制”通俗叫滚动优化。它的核心思想是就算你已经算出了未来24期的全量最优计划真正执行的时候每做完一个周期都要根据实际发生的需求和库存状态把剩下的23期重新优化一遍。我们在B题里的做法完全就是这一套第1期开始前初始化所有参数根据需求情景计算第1期到第24期的最优计划。只执行第1期的采购和生产指令。第1期结束后把实际发生的需求、库存变化写回系统作为新的初始状态。重新优化第2期到第24期再执行第2期指令。这种“每期重算”的方式让模型永远不会被某一次预测大偏差“带偏”。实测下来滚动优化比一次性静态优化综合利润平均高出6%到12%。3. 核心代码实现从数据清洗到动态规划主循环3.1 数据清洗阶段我踩了三个大坑题目附件里的数据表面上很规整但真要直接用它建模会死得很惨。第一个坑是缺货日期的销量是0但这个0不代表“没人买”而是“无货可卖”。如果直接把0当真实需求量喂给预测模型模型会严重低估需求水平。我们的处理办法是把销量为0且当天库存为0的日期标记为缺货日然后在训练预测模型时把缺货日的销量替换成一个“估算需求”用前后几天的均值加一个随机扰动来填充。第二个坑是原材料价格存在周期性。价格序列里有明显的周内波动比如周初价格偏高、周末回落。直接对原始价格做均值回归预测结果会有一天的相位偏移。解决办法很简单把“星期几”作为一个哑变量引入预测模型。第三个坑是产品生命周期的断裂。部分新产品在历史数据的中段才开始有销量早期销量是0。如果把早期0销量当成正常需求量模型会误以为该产品“需求持续走低”导致后期采购决策严重保守。清洗完这些脏数据后模型的表现立刻上了一个台阶。我建议所有参赛队拿到数据后先花2到3个小时仔细做EDA探索性数据分析画折线图、热力图、自相关图把数据里的异常点和业务逻辑对应上再开始建模绝对能帮你避免70%的后期返工。3.2 决策模型的数据结构与状态表示动态规划的代码实现最关键的是如何用数据结构表达“状态”和“决策”。我建议用字典或dataclass来表达而不是用一堆平行的list否则后期调试能让你崩溃。下面是我当时实现的核心数据结构为了帖子简洁我做了一些简化但整体逻辑完全保留from dataclasses import dataclass from typing import Dict, List, Tuple import numpy as np dataclass class PlannerState: period: int # 当前周期 raw_material_stock: Dict[str, int] # 每种原材料库存 product_stock: Dict[str, int] # 每种产品库存 pending_orders: Dict[int, Dict[str, int]] # 已下单未到货的原材料 dataclass class Decision: purchase_plan: Dict[str, int] # 本期采购量 production_plan: Dict[str, Dict[str, int]] # 每条产线上每种产品投产量 class ProductionPlanner: def __init__(self, params): self.params params self.periods params[periods] self.products params[products] self.raw_materials params[raw_materials] self.lines params[lines] def expected_profit(self, state: PlannerState, decision: Decision) - float: 计算在给定状态下采取某决策的期望利润 # 核心逻辑计算销售收入 - 采购成本 - 生产成本 - 库存成本 - 缺货损失 ...这个结构的好处是任何调试时刻我都可以直接打印一个状态对象看清当前是第几期、库存多少、在途订单多少不用对着几十个平行数组猜来猜去。3.3 动态规划的主循环和状态转移实现动态规划主循环说白了就是一个三层嵌套外层遍历周期中层遍历候选状态内层遍历候选决策。但B题的规模决定了不能真三层裸奔必须加剪枝。我的做法是每个周期维护一个state_value_pairs列表里面存着“可达状态”和对应的“最优期望利润”。进入下一周期前先做一次预筛把那些明显低效的状态丢掉。所谓的“明显低效”我定义了两个规则在库存容量没满的前提下原材料库存偏高且近期价格预期走低的状态属于“高库存低价格”的劣势状态优先级下调。产品库存超过预测需求上限120%的状态如果再继续生产同类产品就要接受惩罚性成本。主循环的骨架代码大概长这样def solve(self, initial_state: PlannerState, demand_scenarios: np.ndarray): current_states [(initial_state, 0.0)] for t in range(self.periods): next_states [] for state, profit_to_date in current_states: feasible_decisions self.generate_feasible_decisions(state) for decision in feasible_decisions: new_state, immediate_profit self.transition(state, decision, t) total_profit profit_to_date immediate_profit # 剪枝如果新状态已经存在且利润更高则替换 self.push_or_replace(next_states, new_state, total_profit) current_states self.prune_states(next_states, retain_ratio0.2) best_state, best_profit max(current_states, keylambda x: x[1]) return best_profittransition函数是动态规划的心脏它把当前状态、决策、需求情景输入进去输出下一周期状态和本期利润。我在这个函数里做了三件关键事把到货的原材料加入库存。按生产计划消耗原材料并产生产品库存。把产品库存与需求情景比对计算销量、缺货量和期末库存成本。这里有个细节因为需求是随机情景所以“本期利润”不是确定值而是所有情景下利润的期望。我用了demand_scenarios[t]提供的全部情景做平均而不是只取那条最有可能的中位数路径。3.4 参数设置和调优的一些个人经验需求情景数建议设在200到500条之间。数量太少期望利润的方差太大优化出来的决策不稳定数量太多计算量几何级增长三天三夜都跑不完。库存成本系数在题目里有明确定义但我额外加了一个“超储惩罚项”库存超过仓储容量90%时每多1个单位扣一点期望利润。这样模型会自动把库存压在一个合理区间而不是最后几期堆积成山。对于每类产品我在决策枚举时加了生产批量约束即每次生产量必须是某个最小批量的整数倍。这是从现实中“换线成本”出发加的约束比题目原始约束更贴近业务场景写论文时做灵敏度分析也更有话可说。调参的过程中我最大的心得是不要指望一次性把所有参数调到最优。我的流程是先用一组粗参数把全流程跑通确保代码逻辑没有bug然后逐个修改关键参数观察利润曲线和库存曲线是否“讲得通”最后才做细粒度搜索。很多队伍在调参阶段直接用暴力网格搜索一跑就是几个小时完全是在浪费时间。4. 论文写作与可视化国一与国二之间的分水岭4.1 论文不是代码的说明书而是思维的可视化很多队伍到了第三天下午才开始赶论文最后交上去的东西就是把模型和代码截图拼在一起完全没有任何逻辑主线。这是大忌。我记得我们队伍在开头两小时就用泳道图把整个建模流程画清楚题目理解 → 数据清洗 → 需求情景生成 → 动态规划求解 → 滚动优化 → 灵敏度分析 → 策略建议。然后按这个泳道图来分配每个人负责的章节写作时严格按照这条“业务故事线”走而不是按照代码文件的顺序写。写作时我还坚持了一个原则每展示一个公式必须紧跟一段白话解释。评委大多是数学和运筹学背景他们看公式不费力但他在有限的时间内最想看到的是“你为什么这么建”“这个公式和业务怎么对应起来”。所以我在论文里大量使用“这里的意思是”“换成人话就是”这类过渡句把抽象的符号和具体的业务场景挂钩。4.2 图表做得好评委好感度直接拉满实事求是地说我们论文里的图不算多但每一张图都服务于一个论点。比如有一张是“滚动优化 vs 静态优化”的库存对比图两张曲线放在一起能非常直观地看出我们的算法在中后期库存控制上的优势。还有一张是“不同需求情景下的利润分布直方图”说明我们的策略在不同的市场环境下都能保持不错的利润水平。我见过很多队伍图做得极其华丽3D曲面图、热力动画全上了但图和结论之间毫无关联评委看了只会觉得莫名其妙。图表一定要能直接支撑你的观点而不是为了炫技。4.3 灵敏度分析是拿奖的加分项千万别省略B题里真正能拉开差距的是灵敏度分析。我们在论文中专门用了一章分析原材料价格波动、需求波动幅度、产能利用率三个参数对最优利润的影响。具体的做法是固定其他参数把目标参数从50%变化到150%每变化10%重跑一遍完整的动态规划记录最优利润的变化。然后把结果做成一条敏感性曲线。从这条曲线我们能得出几个很有业务洞察的结论利润对原材料价格波动最敏感但在我们设计的采购策略下价格波动带来的利润损失被明显平滑了。需求波动幅度的加大会导致企业更倾向于“保守生产”牺牲部分高利润产品换取库存稳定。产能利用率的提高能把每个周期的生产安排得更满但边际效益会递减。这些结论不仅证明了我们算法有效更向评委传递了一个信息我们的模型不是死板的数学玩具而是能真正指导业务决策的工具。写完这一章我自己都明显感觉论文的档次上了一个台阶。4.4 论文排版的三个小细节排版这件事看起来是“表面功夫”但作用极大。我们特别关注了三个细节公式统一用LaTeX排版字体大小、编号格式全篇一致绝不在Word里手打公式表格全部使用三线表表头、脚注、数字格式统一参考文献全部是真实可查的学术论文绝不为了凑数乱写。另外一个容易被忽略的地方是附录。我们的附录只放了两种内容核心算法的伪代码和关键程序运行输出的样例。完整源码没有放进论文而是按题目要求单独提交。很多队伍把大段源代码直接贴在附录里既浪费篇幅也影响阅读体验。5. 复盘与避坑指南参赛三天我浓缩出的实战心法5.1 第一天最容易犯的错误是“急着动手写代码”第一天的正确节奏应该是上午读懂题目下午做数据EDA和模型选型晚上才开始搭代码框架。我们是第一天晚上才真正开始写第一版预测代码的。很多队伍第一天上午就开始写爬虫抓数据或者套神经网络模板结果第三天才发现模型结构完全不对回天乏力。我建议第一天一定要留出至少2小时给“团队讨论”。我们三个人一个从业务逻辑的角度提问“这个约束合理吗”一个从算法角度问“这个状态空间怎么压缩”另一个从写作角度问“这个结论能不能在论文里讲清楚”。这三类问题逼着我们把题目从不同的侧面想透了才动工。5.2 第二天要对自己够狠尽早砍掉不靠谱的方案第二天上午我们曾经试过用遗传算法直接搜索整条采购生产计划。实验结果很不理想种群收敛太慢24个周期的联合决策空间太大了跑了1个小时还没有收敛的迹象。当时差点陷进去想着不断调整参数“再试试”。最后是队友一句话点醒了我“这个方案就算调出来了论文里怎么解释遗传算法的参数选择怎么说清楚它比动态规划更好”我们当天中午就砍掉了遗传算法回到动态规划路线。事实证明这个决定极其正确。在国赛的环境里方案的可解释性和可复现性比方案本身的理论上限重要得多。类似的坑还包括用过长的时间去调LSTM的隐藏层维度、两端数据做出来的预测没有业务逻辑、分工不明确导致代码风格混乱等。这些都是第一天规划不到位、第二天没有及时止损带来的连锁反应。5.3 第三天拼的不是智力是体力和心态第三天我们基本是按“写论文6小时改模型4小时合稿3小时查漏补缺3小时”的节奏走的。身体已经非常疲惫了但脑子必须保持清醒。这里有一个心得第三天尽量不要再加入新的模型或者大的算法改动只做“性能优化”和“bug修复”。我们有一个朋友队伍第三天上午突然觉得原本的模型不够“高级”临时换了一个强化学习框架结果下午训练还没跑完论文只能硬着头皮写最后成绩非常不理想。还有一个很实际的经验提交系统在截止前1小时特别容易卡顿一定不要卡点提交。我们当时提前2小时就提交了第一次最后1小时只做了文字微调心态非常稳。5.4 最终的几个小提醒题目要求的输出文件格式一定要先读清楚比如某些表要求保留两位小数某些表要求按特定列名输出。我们有一个队友的朋友队伍程序跑出来的结果是对的但输出格式和题目要求差了一个列名被判为格式错误直接扣了很多分。模型假设要明确写在论文里。比如“原材料在途期间不发生损耗”“不考虑紧急补货渠道”等不要“不好意思”写假设。有明确的假设评委才知道你的模型边界在哪里。所有随机数要设置固定的random.seed。我们当时每次跑出来的结果都不完全一样后来统一加了seed保证代码可复现。这个细节虽然不大但很多评委会在意你的程序是否能稳定复现论文里的数值。6. 关于源码和原创论文如果你想直接参考这篇文章里展示的代码只是完整源码的骨架版本。真正完整的可运行版本包括数据清洗、均值回归参数估计、动态规划主循环、滚动优化封装、灵敏度分析脚本以及最终生成提交表格的代码我在整理后已经把全套源码和原创论文打包好了。完整版源码主要有四个文件data_preprocess.py、forecast.py、optimizer.py、run_pipeline.py。运行顺序就是逐行执行run_pipeline.py它会自动调用前三个模块输出最终的生产采购计划表。整个流程在普通笔记本上跑一轮大约需要25到35分钟完全可接受。我做了一个压缩包里面还附带了我们的最终论文PDF标注了每一章的写作重点和评委可能的提问方向。由于平台不方便直接挂资源链接你可以私信我发送“B题源码”四个字我会把下载方式和配套说明文档发给你。如果你只是对某个具体模块感兴趣比如只看预测部分的完整实现或者只看动态规划的剪枝策略也可以直接跟我说我可以单独拆一部分详细讲。这篇复盘文章很多内容都是当时“流着血”换来的经验。数学建模国赛每年都有人拿国一但每一篇国一论文背后一定有一套清晰的方法论和无数次的调试迭代。希望我的这些分享能让你少走一些弯路把时间和精力花在真正能拉开差距的地方。如果你也在准备数学建模或者正在为某个决策优化类题目头疼欢迎在评论区留下你正在处理的具体问题。我看到后会挑有代表性的问题结合我的源码和论文经验再出几篇针对性的拆解。祝各位都能拿到理想的成绩。
返回列表