☰
数学建模国赛B题国一攻略:动态规划与滚动优化实战解析
2026/10/7 9:06:18 网站建设 项目流程

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, dt=1.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, rcond=None) 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_sim=1000, dt=1.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期开始前,初始化所有参数,根据需求情景计算第1期到第24期的最优计划。
  2. 只执行第1期的采购和生产指令。
  3. 第1期结束后,把实际发生的需求、库存变化写回系统,作为新的初始状态。
  4. 重新优化第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_ratio=0.2) best_state, best_profit = max(current_states, key=lambda x: x[1]) return best_profit

transition函数是动态规划的心脏,它把当前状态、决策、需求情景输入进去,输出下一周期状态和本期利润。我在这个函数里做了三件关键事:

  1. 把到货的原材料加入库存。
  2. 按生产计划消耗原材料,并产生产品库存。
  3. 把产品库存与需求情景比对,计算销量、缺货量和期末库存成本。

这里有个细节:因为需求是随机情景,所以“本期利润”不是确定值,而是所有情景下利润的期望。我用了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题源码”四个字,我会把下载方式和配套说明文档发给你。如果你只是对某个具体模块感兴趣,比如只看预测部分的完整实现,或者只看动态规划的剪枝策略,也可以直接跟我说,我可以单独拆一部分详细讲。

这篇复盘文章,很多内容都是当时“流着血”换来的经验。数学建模国赛每年都有人拿国一,但每一篇国一论文背后一定有一套清晰的方法论和无数次的调试迭代。希望我的这些分享,能让你少走一些弯路,把时间和精力花在真正能拉开差距的地方。

如果你也在准备数学建模,或者正在为某个决策优化类题目头疼,欢迎在评论区留下你正在处理的具体问题。我看到后会挑有代表性的问题,结合我的源码和论文经验,再出几篇针对性的拆解。祝各位都能拿到理想的成绩。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询