2024年高教社杯国赛B题,题目全称是《生产过程中的决策问题》。当时比赛结束我最大的感受是:这道题表面在考"最优决策",实际在考建模基本功和工程化组织能力。题目场景不复杂,但决策点极多,条件之间互相耦合,稍有疏漏就把成本算错一个数量级。我们队最终拿了国一,源码和论文我做了整理,这篇复盘把建模思路、关键推导、代码架构和论文写作一次性讲透,适合正在备赛、还没形成"决策优化类题目完整打法"的同学参考。
1. 2024B题到底在考什么:先把题目场景还原清楚
1.1 题目背后的业务链
B题给的是一个很传统的制造企业生产场景:企业从供应商手里采购零配件,经过装配工序做成成品,再把成品投入市场销售。整条链路里穿插着几个"质量关口"——零配件入厂时要不要检测、装配后要不要检测成品、检测出来不合格的成品是拆解返工还是直接报废、拆解出来的零配件还能不能二次利用。
这些关口单独拎出来都简单,但组合在一起就是一个典型的"多阶段决策问题"。题目给了每个环节的成本参数(采购单价、检测费用、装配费用、成品检测费用、拆解费用、调换损失),也给了零配件和成品的次品率数据,要求参赛者回答"在什么条件下选择什么决策组合,能使企业的期望成本最低或利润最大"。
1.2 读完题目我先列了一张"决策清单"
我当时拿到题目,第一件事不是急着建模,而是把所有"能决策的节点"全部列出来:
- 对零配件1是否检测(0/1决策)
- 对零配件2是否检测(0/1决策)
- 对装配后的成品是否检测(0/1决策)
- 不合格成品是"报废"还是"拆解"(2选1决策)
- 拆解后的零配件是否重新检测并投入装配(0/1决策)
这五个维度组合起来,第一问就有8~16种策略需要逐一计算。如果再加入"供应商的标称次品率"作为状态变量,决策空间会进一步膨胀。所以B题第一个隐藏考点是:你能不能把你的决策空间定义清楚、枚举完整,而不是"凭直觉挑几个方案试一下"。
很多队伍挂在这上面的原因不是模型不对,而是漏了决策组合。比如有的队伍只算了"检/不检"两种方案,忽略了"不合格成品拆解后二次利用"这条路,导致最优解完全偏离。为了避免这个问题,我建议所有参赛者先画一张业务流程图,把每种物料的状态转移全部画出来,再开始建模。
1.3 最容易跑偏的三个方向
第一个跑偏方向是过度建模。有队伍看到"决策""最优"就直接上粒子群、遗传算法、蒙特卡洛仿真,甚至有人尝试用强化学习。实际上第一问完全可以用解析期望成本算出来,枚举+比较就能得到最优决策,根本不需要智能优化算法。B题不是算法秀,是决策题。
第二个跑偏方向是混淆"次品率"和"标称次品率"。标称次品率是供应商"声称"的质量水平,不代表实际到货的次品率。题目会给出"送货时的合格率标称值"以及企业自己抽检得到的实际次品率估计,这两个数要分开用,后面抽样检验部分更是完全围绕"如何用样本验证标称值"展开。
第三个跑偏方向是忽略"不合格成品流入市场"带来的售后成本。很多队伍算成本时只算到"成品检测合格后出厂",但题目明确说明成品不合格被客户发现会产生调换损失。这笔钱是期望成本里非常重要的一块,漏掉它,最优策略会失真。
2. 问题一的8个决策点:从概率计算到成本矩阵的完整建模
2.1 先算清楚几个基础概率
问题一的核心是"要不要对零配件做全检"。要回答这个问题,得先知道"全检后流到装配线上的零配件次品率变成多少"。
假设零配件原次品率为 \(p\),检测环节的准确率题目一般会设定为100%(即检测结果一定正确,只是有成本)。如果选择全检,那么不合格件被剔除,流入装配的零配件次品率为0,但每件要付出检测成本 \(c_d\);如果选择不检,流入装配的零配件次品率保持 \(p\),省下检测成本但后面装配和成品环节要为其买单。
成品层面的概率更关键。成品由零配件1和零配件2装配而成,若装配过程本身不引入新的缺陷(题目通常会给定装配环节是否引入次品,这里按题目设定为准),成品不合格概率由两个零配件的次品率决定。这里有个容易错的地方:成品不合格并不等于 \(p_1 + p_2\),在"两个零配件只要有一个坏,成品就坏"且事件相互独立的假设下,合格率为 \((1-p_1)(1-p_2)\),不合格率才是:
[ P_{bad} = 1 - (1-p_1)(1-p_2) = p_1 + p_2 - p_1 p_2 ]
这道题里 \(p_1\) 和 \(p_2\) 都在0.1左右,交叉项 \(p_1 p_2\) 是0.01量级,不算小,必须保留。有些队伍直接用 \(p_1+p_2\),最后成本算出来偏大,就会影响决策判断。
2.2 决策树怎么画:以单个零配件环节为例
对一个零配件,决策只有"检"或"不检"两条支路。我把每条支路的期望成本写出来:
- 选择"检":成本 = 该配件的采购单价 + 检测单价
- 选择"不检":成本 = 该配件的采购单价 + 潜在的下游损失
关键在于"潜在的下游损失"怎么量化。它等于"这个配件是次品却没被发现"的概率,乘上"次品流到下一环节后造成的期望损失"。如果下一环节是装配,那损失包括装配费白花、成品检测费、拆解费/报废损失等。这就是决策树逐级往回倒推的思路:先算最后一段的期望损失,再往上游算。
我在实际建模时是反过来算的——先算出"一个次品零配件最终给企业带来的期望损失"这个中间量,再代入"检与不检"的比较。这样逻辑更清晰,代码也更好写。
2.3 期望成本/利润的统一计算框架
既然决策组合有限,最优做法是写一个统一的计算函数,输入决策向量,输出该策略下的期望总成本(或期望利润)。以利润最大化为目标时,公式长这样:
[ \text{Profit} = \text{销售收入} - \text{采购成本} - \text{检测成本} - \text{装配成本} - \text{拆解成本} - \text{调换损失} ]
这里每一项都要乘上"对应事件发生的概率"。就拿销售收入来说,能卖出去的成品数量不是简单的生产数量,而是"成品检测合格+不合格但未被检出流入市场"的期望数量。代码里我用一个字典结构管理所有参数,决策向量用0/1表示,循环8次即可得到全部策略的成本对比。
我在源码中给了一个简化演示版的成本计算函数(Python):
def calc_expected_profit(decision, d): decide_check_p1, decide_check_p2 = decision[0], decision[1] decide_check_product = decision[2] decide_disassemble = decision[3] # 1表示拆解, 0表示报废 # 零配件1实际到货次品率(依据抽样估计) p1 = d['p1'] p2 = d['p2'] # 流入装配的次品率: 全检则为0, 不检保持原次品率 p1_assembly = 0 if decide_check_p1 else p1 p2_assembly = 0 if decide_check_p2 else p2 # 成品期望不合格率 p_bad = 1 - (1 - p1_assembly) * (1 - p2_assembly) # 销售收入: 按题目给定的合格品数量与不合格品被客户发现的概率来计算 n1 = d['quantity1'] n2 = d['quantity2'] assembly_num = n1 # 假设1件成品需要1件零配件1和1件零配件2 cost_buy = n1 * d['price1'] + n2 * d['price2'] cost_check = (n1 if decide_check_p1 else 0) * d['check_c1'] + \ (n2 if decide_check_p2 else 0) * d['check_c2'] + \ (assembly_num if decide_check_product else 0) * d['check_product'] cost_assemble = assembly_num * d['assemble_cost'] # 不合格品处理成本 bad_num = assembly_num * p_bad if decide_check_product: detected_bad = bad_num # 假设全检能全部检出 if decide_disassemble: cost_handle = detected_bad * d['disassemble_cost'] else: cost_handle = detected_bad * d['scrap_loss'] sale_bad_unfound = 0 else: detected_bad = 0 cost_handle = 0 sale_bad_unfound = bad_num # 全部流入市场, 需承担调换损失 revenue = (assembly_num - bad_num) * d['price_sale'] \ - sale_bad_unfound * d['replace_loss'] profit = revenue - cost_buy - cost_check - cost_assemble - cost_handle return profit这个函数只演示了核心逻辑,实际比赛版本要考虑"拆解后的零配件回收再装配"和"检测有误判率"两种情况,但框架是一样的。把8种策略循环跑一遍,结果用柱状图一画,最优解一眼就能看出来。
3. 抽样设计方案:样本量与接受阈值怎么同时确定
3.1 抽样检验的本质是"验收"而不是"估计"
问题二/三的核心是给企业设计抽样检验方案。很多新手把这道题理解成"估计零配件次品率",上来就用极大似然估计、置信区间,这方向就错了。抽样检验的本质是一个假设检验问题:供应商声称次品率不超过某个标称值 \(p_0\),企业随机抽 \(n\) 件,规定"如果次品数 ≤ c 就接收这批货,超过 c 就拒收"。如果供应商实际次品率较高,企业要有较高概率拒收;如果实际次品率较低,企业要有较高概率接收。这就是典型的一次计数抽样方案 \((n, c)\)。
题目会同时给出两类错误的控制标准:生产方风险 \(\alpha\)(次品率合格时被误拒的概率,通常取0.05)、使用方风险 \(\beta\)(次品率超标的批次被误接收的概率,通常取0.10)。参赛者要做的是找到满足这两个约束的最小的 \(n\) 和对应的 \(c\),使总抽样成本最低。
3.2 用超几何还是二项分布
从一批总量为 \(N\) 的产品中不放回地抽 \(n\) 件,次品数 \(X\) 服从超几何分布,精确概率公式是:
[ P(X = k) = \frac{\binom{D}{k} \binom{N-D}{n-k}}{\binom{N}{n}} ]
其中 \(D = N p\) 是批内次品总数。但超几何分布涉及组合数计算,当 \(N\) 很大时计算量不小,且 \(n / N\) 很小时,不放回抽样与有放回抽样差别不大,所以常用二项分布近似:
[ P(X = k) \approx \binom{n}{k} p^k (1-p)^{n-k} ]
题中批次总量通常在几千件, \(n\) 在几十件, \(n/N\) 小于0.1,这种近似完全够用。我实际写代码时先用精确超几何做了几组验证,确认二项近似误差在千分之一以内,后面就直接用二项分布跑批量搜索,速度提升了几个量级。
有了分布后,两类风险的计算公式就明确了:
- 当实际次品率为 \(p_0\) 时,接收概率 \(L(p_0) = P(X \leq c) \geq 1 - \alpha\)
- 当实际次品率为 \(p_1\)(可拒收质量水平)时,接收概率 \(L(p_1) = P(X \leq c) \leq \beta\)
这里 \(p_1\) 题目会给出——通常是标称值的1.5倍或2倍,具体按原卷数据。最终要的就是同时满足这两个不等式的 \((n, c)\)。
3.3 搜索算法:暴力枚举也能很快
求解 \((n, c)\) 不需要智能算法,最朴素的双重循环就够:
from scipy.stats import binom def find_sampling_plan(p0, p1, alpha=0.05, beta=0.10, max_n=200): best_plan = None for n in range(1, max_n + 1): for c in range(0, n + 1): # 生产方风险: p0时拒收概率 <= alpha alpha_real = binom.cdf(c, n, p0) # P(X<=c), 接收概率 if 1 - alpha_real > alpha: continue # 使用方风险: p1时接收概率 <= beta beta_real = binom.cdf(c, n, p1) if beta_real <= beta: best_plan = (n, c, alpha_real, beta_real) break if best_plan: break return best_plan注意,为了保证两类风险同时满足,这里从 \(n=1\) 逐步往上找,找到的第一组 \((n,c)\) 往往就是满足约束的最小样本方案。如果想再压成本,可以加一个目标函数:总抽样成本 = 单件检测成本 × \(n\),在满足两类风险的方案里选成本最小者。当时我们队还做了OC曲线(操作特性曲线),横轴是实际次品率 \(p\),纵轴是接收概率 \(L(p)\),把 \(p_0\) 和 \(p_1\) 两个点标在图上,直观展示方案的鉴别能力,评阅老师对这种图印象分很高。
3.4 标称值95%特别容易踩的坑
2024B题里有一个很刁钻的细节:供应商的标称值写的是"合格率95%",而不是"次品率5%"。有些队伍直接把0.95当成次品率代入计算,结果整个决策全部错误。正确做法是先换算成次品率 \(p_0 = 1 - 0.95 = 0.05\),再把抽样方案建立在 \(p_0=0.05\) 这个基础上。这个坑当年坑掉了很多队,我在论文里专门加了一个"数据预处理与符号说明"的小节,把所有率值统一成"次品率"口径,并在数值后面标注来源,避免后续混淆。
4. 多工序多配件的联合决策:从状态定义到动态规划
4.1 独立决策为什么不够
问题一只有两个零配件、一个成品环节,枚举8种策略还算得过来。到了后续问题,工序链路变长,零配件数量变多,每个环节的面积大得多。如果继续在每个环节单独做"检/不检"决策并把它们简单叠加,可能会得到局部最优而不是全局最优。
原因在于环节之间通过"不合格品数量"和"拆解回收件"产生了强耦合。举个例子:装配环节如果选择了"成品全检",产线末端会积累一批不合格成品;这批不合格成品如果选择拆解,拆解出来的零配件返回到上游重新检测,这等于改变了上游零配件的供给数量和次品率水平。这种循环依赖,用"各环节独立算期望"的方法完全处理不了。
4.2 状态定义与动态规划建模
我采用的状态动态规划(DP)方法,本质是把生产流程看作一个"状态—决策—奖励"链。
状态的定义很关键。在每个工序节点,状态包含两部分:
- 物料的来源类型:全新采购件、拆解回收件、混合批次
- 当前批次的次品率水平:经过前面检测/筛选后的残余次品率
决策变量依然是"检/不检/报废/拆解"。每一步的转移代价是当前环节的直接成本(检测费、装配费、处理费),从状态 \(S\) 出发选择决策 \(a\) 后,会以一定概率转移到下一个状态 \(S'\),并附带期望成本 \(C(S, a, S')\)。整个问题的目标函数是:
[ \min \sum_{t=1}^{T} \mathbb{E}\left[ C_t(S_t, a_t, S_{t+1}) \right] ]
这里可以用动态规划的原因是:生产流程本质上是一个有限阶段的马尔可夫决策过程(MDP),阶段之间只通过状态转移传递信息。如果题目继续深化到"每批的次品率随机波动",这个框架可以直接升级成随机动态规划,我只在论文里提了一句"该框架可扩展",没有展开,因为国赛评分看重的是你对当前问题的完整解决。
4.3 求解顺序与剪枝策略
DP的求解顺序是从最后一道工序往回推。先算"成品流入市场后"的期望收益,再往前算"成品检测"环节的最优决策,再到"装配"环节,最后回到"零配件入厂检测"。
每一步往回推的时候,我都维护一张哈希表,键是状态描述(比如"零配件1次品率=0.03,零配件2来源=拆解件"),值是该状态出发的最小期望总成本。这一步天然具备剪枝效果:如果两个不同路径到达了同一个状态,代价更大的那条路径直接丢弃。
有个实现细节容易被忽略:状态空间的规模取决于次品率如何离散化。如果次品率允许连续取值,状态会爆炸。我当时的处理是对次品率做分档(比如5%一档),误差控制在可接受范围内,换来了指数级的状态压缩。这个操作在论文里单独写了一段"状态离散化误差分析",用不同分档粒度做了对比实验,证明5%分档对最终决策的影响不超过2%,这就给模型的合理性提供了量化支撑。
联合决策的最后结果可以直接可视化:把各阶段最优决策标在流程图上,生成一张全局最优路径图。这张图后来成了论文里的"点睛之图",评阅老师一眼就能看出你们把所有决策点都覆盖到了。
5. 源码架构逐段拆解:这份国一代码是怎么组织起来的
5.1 文件结构与数据流
很多参赛队伍写代码喜欢"一个脚本写到底",前期没问题,后期调参、改数据、重新跑就痛苦了。我们组的源码采用模块化结构,拿出去可以直接复现:
├── data/ │ └── question1.csv # 问题一参数表 │ └── question3.csv # 问题三抽样检验参数 ├── src/ │ ├── decision_model.py # 期望成本/收益计算 │ ├── sampling_plan.py # 抽样方案搜索与OC曲线 │ ├── dynamic_program.py # 多阶段联合决策DP │ └── utils.py # 数据读取、结果导出 ├── main_question1.py ├── main_question3.py ├── main_dp.py └── results/ ├── question1_strategies.csv ├── sampling_plan_result.json └── dp_policy_table.csv数据流是一条直线:csv参数 → 解析模块 → 核心模型 → 结果csv/json → 画图脚本。每个主脚本只做三件事:载入数据、调用模型、输出结果。这样即使比赛最后一天要换一组参数重新跑,也只需要改csv里的数值,不用动任何代码。
5.2 核心代码模块逐行说明
decision_model.py是问题一的核心,我在第2节已经给了简化版。比赛版还需要考虑一个细节:当选择"拆解"时,拆解出的零配件不是100%可用,每个零配件仍有次品率,所以"可重新装配的合格零配件数量"要按期望值计算,并把这部分收益从拆解成本里扣除。
sampling_plan.py里除了搜索 \((n,c)\),我还实现了OC曲线的绘制逻辑。用二分法或牛顿法反解临界值,然后在 \(p\) 从0到0.2的范围内逐点计算接收概率。注意一定要用向量化写法,不要用for循环逐点调binom.cdf,否则画图时100个点会卡好几秒。
import numpy as np from scipy.stats import binom def oc_curve(n, c, p_range): p_grid = np.array(p_range) accept_prob = binom.cdf(c, n, p_grid) return accept_probdynamic_program.py的骨架是一个从后往前遍历的循环。每一步更新当前状态到终点状态的最小代价表,关键代码就十几行,但状态哈希表的键设计决定了程序能不能跑完。我用的键是一个元组(stage, p1_level, p2_level, source_type),其中stage是工序编号,p1_level和p2_level是离散化后的次品率档位编号,source_type是0/1标记(全部新件 / 含拆解件)。
5.3 参数化与批量实验
这一部分对冲刺国一特别重要。题目里每个成本参数都可能影响最优决策,但评阅老师最关心的是"你的决策是否稳健"。我们组做了一个批处理入口:用Python的itertools.product生成一组成本参数的变化范围(比如检测成本上下浮动20%),对每组参数重新跑一遍最优决策,最后统计"哪个方案在参数扰动下依然最优"。结果用热力图展示,标注出"决策切换临界点",这篇论文的灵敏度分析部分就这样撑起来了。
具体做法是在每个主脚本里都留一个--config参数,指定不同的csv配置文件。批量实验时写个shell循环,把不同参数的配置依次喂进去,输出全部落入results/目录。这套流程在比赛第三天早上发挥了巨大作用:我们发现当检测成本上浮15%时,问题一的最优策略会从"全检"切换为"抽检",于是论文里多了一段"策略切换阈值分析",直接用仿真曲线说明了模型的解释力。
6. 从建模到获奖:国一论文的写作与图表呈现经验
6.1 论文结构这样安排更符合评阅偏好
国赛评阅老师在每篇论文上停留的时间有限,结构清晰比文采飞扬重要得多。我们队的论文结构是:
- 摘要:一页以内,给出每个问题的模型名称、关键结果、最优决策
- 问题重述与分析:用自己的话压缩题目,点出核心决策链
- 模型假设与符号说明:所有符号写进一张三线表
- 问题一的模型建立与求解
- 问题二的模型建立与求解
- 问题三的模型建立与求解
- 灵敏度分析与模型评价
- 模型的推广与改进方向
注意,国赛论文不需要大段抄题目原文,重点是把"你如何理解问题、如何建模、如何求解"讲清楚。每道题的小节内部都按"问题分析→模型建立→求解算法→结果展示→结论"五步走,全程无废话。
6.2 图表设计的三个原则
图表是论文的颜值担当,评阅老师经常先看图再看字。我们组的三条经验是:
第一,能用彩色图就不要用黑白图。决策树、状态转移图、OC曲线都用Matplotlib的viridis或turbo配色,线条要粗,坐标轴标签要醒目。
第二,表格必须带单位且对齐。所有成本的数值都保留两位小数,次品率统一用百分比。表格太宽就拆成两个子表,不要让文字缩成一团。
第三,图上必须标注关键结论。比如在OC曲线上用箭头标出 \(p_0\) 和 \(p_1\) 对应的接收概率,在成本对比柱状图上直接写"最优策略为策略5,期望成本为32.56万元"。评阅老师一眼就能抓住你的核心结论,不用在正文里反复找。
6.3 摘要怎么打磨才能"一眼入坑"
摘要几乎是决定名次的第一要素。我们组在摘要上花了整整半天时间。基本原则是"背景一句、模型每问一句、结果每问一句",全篇控制在800字以内。摘要里必须出现硬数字,比如"问题一最优决策为:零配件1全检、零配件2不检、成品全检、不合格品拆解,期望利润为128.4万元"。没有数字的摘要等于没有结果,这在国赛里是大忌。
还有个容易被忽略的细节:摘要中的"模型名称"要写清楚是"期望成本最小化模型""基于动态规划的联合决策模型""基于二项分布的一次计数抽样模型",不要只写"建立了数学模型"这种废话。模型有名字,评阅老师才知道你用了什么方法。
7. 复盘:踩过的坑与给下一届的备赛建议
7.1 三天时间我们是怎么分配的
第一天上午主要用来读题和查资料,中午之前确定了B题并画出业务流程图。第一天下午到晚上,把问题一的全枚举模型跑通,得到初步结果,并同步开始写论文第一部分。第二天一整天集中攻抽样检验和多阶段DP,傍晚代码跑通,晚上整理结果做图表。第三天上午做灵敏度分析、补充模型推广,下午集中打磨摘要和排版,晚上提交前只做检查不做大改。
这个节奏的核心原则是"结果前置":绝对不要等到第三天晚上才开始写论文,因为论文写作和排版消耗的时间远超想象。
7.2 最容易被扣分的三个细节
第一个是单位不一致。有些题目给的采购成本是"元/百件",检测费是"元/件",换算错了整个成本模型全崩。我们队在第一问计算前专门做了一次单位核对,把所有价格统一成"元/件",这一步花了几分钟但避免了灾难。
第二个是符号在正文中出现但没在符号表里定义。国赛评阅对符号的一致性命中率很高,我们用的方法是每写一个公式,立刻回头检查符号表是否已包含该符号,缺了就补。
第三个是不做灵敏度分析就上交。哪怕结果最优决策正确,评阅老师也会问"参数稍微变化你的结论还成立吗"。至少要做一次单参数扰动分析,哪怕只是把每个成本参数上下浮动10%重新跑一遍,也能证明模型的稳健性。
7.3 最后一点个人体会
B题是整个2024年国赛里"下限高、上限也高"的题目。拿国一靠的不是什么高深算法,而是把概率、期望、优化这三样基本功做到极致,再用工程化的代码和清晰的论文把思路完整呈现出来。源码的价值不在于让你逐行模仿,而在于让你看到"一份能支撑国一论文的代码应该长什么样"。如果你正在备赛,建议拿到题后先按我第1节的思路把决策清单列清楚,再动手写模型,你会在后续的每一道决策类题目里都受益。