ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数学建模竞赛A题全攻略:从审题建模到代码论文的完整流程

数学建模竞赛A题全攻略:从审题建模到代码论文的完整流程

数学建模竞赛的A题,向来是大多数参赛队首选的攻坚对象,也是很多人最怵的一题。我自己带过队伍、也亲自上场写过代码,每年被问得最多的就是:“A题到底怎么找思路?”这篇内容就把我从审题、建模、写代码到写论文的完整流程拆开讲一遍,全是实操层面的经验。不管你是第一次参赛的本科生,还是想系统复盘的老手,这套方法都值得对照着用一次——尤其是当你手上只有题目、还没想好从哪落笔的时候。

1. 竞赛A题在考什么:先搞清楚出题人的意图

1.1 A题不只是一个模型,而是一串模型组合

很多初学者拿到A题后,第一反应是“这题该用什么模型?”——这是最典型的误区。出题人不会只考一个孤立模型,他给的是一段场景描述,背后往往藏着三到五个关联问题:数据怎么处理、核心关系怎么建模、参数怎么估计、结果怎么验证。你推演一下,通常一个A题的前几问是数据清洗和基本统计,中间几问是模型构建,最后一两问才是综合应用。所以“一上来选模型”会让你卡在中间,因为前面步骤都没走,问题根本套不进去。

我自己第一次参赛时就吃过这个亏。那年看到一道和水库调度有关的题,直接开始用动态规划,结果连上游流量的数据都没清洗干净,绕了一大圈才发现,题目先要的是对历史数据的拟合,根本不是优化调度。那次之后我得了一个教训:先看每一小问在问什么,把它们按“数据层、模型层、应用层”列出来,再决定每一层怎么处理。这个习惯后来帮我节省了大量返工时间。

1.2 题干里真正有用的信息藏在“条件”和“限制”里

做题时我会拿一支笔,把题干中出现“不超过”“至少”“尽量”“在……条件下”这些词全部圈出来。它们就是约束条件;没有这些词的部分,是背景介绍,可以快速略过。第二步是把“要求某结果尽可能大/小”这类句子改写成目标函数。

例如,题干说“要在总费用不超过这个预算的前提下,使服务质量尽量高”,这就等于一个最小化成本、最大化服务的双目标问题。如果你能在读题阶段就把这种话翻成“变量+约束+目标”,建模就有了支架,后面塞方程会顺畅很多。很多队伍之所以后面写着写着跑偏,就是因为一开始没有把这种约束关系抠出来。

我还习惯在纸上画一个简单的逻辑图,把题目里出现的实体用箭头连起来,标上哪些是输入、哪些是输出、哪些是过程约束。这张图不用给别人看,自己画完,整个问题的结构基本就清晰了。

1.3 近年A题的新变化:数据量和交叉度都在涨

从趋势看,出题方向越来越倾向于真实数据,文件动辄几千行,字段也挺杂,可能有时间戳、地理位置、传感器读数等。再加上跨学科内容增多,环境、交通、生物、金融都可能出现。因此,2025年备赛不能只练数值计算,还要熟悉数据清洗、缺值处理、异常值筛查这些操作。建模倒是其次,能否把脏数据洗干净常常决定你能走多远。

我见过不少队伍,模型选得很高级,结果数据读进来之后有大量缺失值,回归根本跑不动,最后只能临时改成均值填充。这个细节非常影响论文的下限:如果连数据描述都写得含糊,评委很难相信后面的结论。所以建议备赛时多练几种“脏数据”处理场景:时间序列缺失、空间数据漂移、采样频率不一致、单位不统一等。

2. 破题的核心四步:拆、抽、选、落

2.1 拆:把一段场景描述切成“问题链”

拿到题目后,我会先把所有小问编号,用小标题概括每个问题的本质。这个过程听起来简单,但它能避免“答非所问”。例如,第一问是“对某指标的变化规律进行分析”,它的本质是回归或时间序列;第二问是“设计一个方案”,本质是优化;第三问是“评估方案在不同场景下的表现”,本质是灵敏度分析。一旦你给每问贴上本质分类标签,选模型就不是猜了。

建议整个队伍先一起做“拆”这一步,每个人写一版问题链,然后再合并。这样速度快,而且不会有人从第一步就歪掉。如果队伍里有比较细心的人,可以让他专门负责画问题依赖关系,因为很多题的第三问其实是建立在第二问模型基础上的,如果你在第二问就把结构定了,第三问相当于顺水推舟。

2.2 抽:把文字翻译成变量、参数和关系式

这是最难但最值钱的一步。把场景中的实体变成变量,把约束变成不等式,把目标变成函数。举个例子,一个简单的库存问题:需求量D是参数,订购量Q是决策变量,单位库存成本h是参数,那么总成本C(Q) = 订购成本 + 存储成本,目标就是最小化C(Q),同时Q≥0。当你每个变量都定义清楚,模型就已经完成了一半。

我能给出的实操建议是:先写一张“变量表”,三列:符号、含义、类型(决策/参数/中间变量)。模型写完前,变量表不要关闭,随时补充。论文里放这张表也很加分,评委会觉得你逻辑清楚。这里有一个很多人忽略的小问题:变量符号务必全篇统一。有人正文用大写X,公式里又写成小写x,图表里又变成别的符号,这种不一致会让评委直接失去耐心。

2.3 选:模型选型不是靠感觉,而是靠“数据结构+目标形式”

模型选型有一个很实用的判断顺序:如果目标是数值连续,考虑回归、插值或时间序列;如果目标是离散方案,考虑整数规划、图论;如果问题有随机性,考虑蒙特卡洛或随机规划;如果数据量大但机理不清,考虑机器学习。下表是我个人常用的快速对照表:

问题特征常见模型典型工具
连续预测多元线性回归、ARIMA、LSTMsklearn, statsmodels
分类判别逻辑回归、决策树、SVMsklearn
离散优化整数规划、动态规划scipy.optimize, ortools
调度路径图论、遗传算法、模拟退火networkx, 自写启发式
机理仿真微分方程、元胞自动机scipy.integrate, 自写仿真

这个表不是标准答案,但能帮你节约大量试错时间。遇到一个模型算不动时,别急着调参,先用这个表判断是不是选型方向错了。比如你一直用随机森林去做一个本质是排序优化的问题,那肯定会被评委抓住把柄,因为你根本没有解释清楚目标函数的数学形式。

2.4 落:算法落地前先想清楚“算得动”和“够用”

很多团队卡在“模型完美但三天跑不出结果”。所以我一般规定:先实现一个最朴素的版本,哪怕只是小规模测试,确认数据能流动,再去追求精度。例如,整数规划用精确求解器跑不完时,就写一个贪心或遗传算法的基线版本。这个“基线优先”原则在比赛里非常救命。碰见大规模问题一定先问自己:是不是可以降维、分块、或把全局优化拆成局部寻优?

很多题目里的数据规模其实没有那么夸张,但如果你把所有约束都完整地丢进一个模型里,就会让求解时间爆炸。更好的做法是:先做约简分析,看看哪些约束是紧约束,哪些是软约束。软约束可以在初始模型里暂不考虑,等结果稳定后再加上去对比。这个过程不仅能提速,还能让你在论文里多写一段“约束松弛分析”,属于加分项。

3. 代码实现:数据处理永远先于模型调参

3.1 先写数据预览脚本,再谈建模

我的习惯是,拿到数据后先不做任何建模,只跑一个脚本:读入数据、打印行列数、看字段类型、统计缺失比例、看基础分布。这个脚本会花掉半小时,换来后面少踩一半的坑。很多人直接开始写模型函数,等到运行时才发现日期列是字符串、空值让回归直接崩塌,来回调错,浪费的时间远超半小时。

关于缺失值,补法要贴合题目:时间序列用插值或前向填充,离散类用众数,连续变量且缺失率低时可以删除该行。切忌全场一招,比如到处都用均值填充,很容易让评委觉得不严谨。我建议每个预处理操作都留好记录,论文的“数据说明”部分可以写“某字段缺失率约3%,故采用线性插值补全;某分类字段缺失率约15%,采用众数替代”,这样评审一看就知道你处理过、也知道你为什么这样处理。

3.2 建模代码的骨架:一个可以复用的工作流

我这里给一个通用的回归预测代码框架,结构也可以平移用于其他问题。示例代码会包含“导入数据→切分样本→选择模型→交叉验证→输出评估”。

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, cross_val_score from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score df = pd.read_csv("data.csv") # 基础预处理:只展示片段,实际要按变量类型分别处理 df = df.dropna(subset=["target"]) X = df.drop(columns=["target"]) y = df["target"] # 切分 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 模型 model = RandomForestRegressor(n_estimators=200, max_depth=8, random_state=42) model.fit(X_train, y_train) # 验证 cv_score = cross_val_score(model, X_train, y_train, cv=5, scoring="neg_mean_squared_error") y_pred = model.predict(X_test) print("CV MSE:", -cv_score.mean()) print("Test R2:", r2_score(y_test, y_pred))

这套骨架贵在把训练、验证和测试分开。很多人一条代码把全部数据喂进去训练,最后只看训练集误差,等于自欺欺人。比赛题目虽然没有硬性要求做测试集,但至少要做交叉验证,否则结果说明不了任何泛化能力。如果题目本身没有给“真实答案”来验证,你也可以随机留出一部分数据当测试集,或者按照时间顺序把前面数据训练、后面数据验证。

3.3 结果可视化:评委看图比看公式更快

写论文前我建议先用matplotlib把关键结果图做出来。常见的几类:

  • 时间序列结果配“历史值+预测值”双线图,一眼看出拟合情况;
  • 优化结果配“决策变量随迭代变化”的收敛曲线,证明你的算法真的在收敛;
  • 灵敏度分析配“参数变动→目标值变动”的折线图,让结论一目了然;
  • 相关性热力图直接展示变量之间的关系,用于探索性分析。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) plt.plot(y_test.values, label="true", linewidth=2) plt.plot(y_pred, label="pred", linestyle="--", linewidth=2) plt.legend() plt.title("True vs Pred") plt.xlabel("sample index") plt.ylabel("target value") plt.tight_layout() plt.savefig("pred_plot.png", dpi=150)

注意图上面一定要有标题和轴标签,别让评委猜图里画的是什么。比赛里我见过很多团队图做得很好看,但没有单位,这属于细节失分。尤其是涉及物理量的图,横轴纵轴都要写清楚单位,否则图再精美,答辩时也会被当场质疑。

3.4 关于随机种子和可复现性

有些模型,尤其是随机森林、神经网络,每次运行结果不一样。所以代码里只要用到随机数,我都会固定random_state;数据洗牌、切分也一样。这样不仅方便自己调试,也方便答辩时当场验证。论文附录要给出代码运行环境,比如Python版本、关键库版本,也是很多评审团队会看的细节。

我还建议把每次实验的关键指标存到单独的文件里,比如“r2_log.txt”,每跑完一个模型就追加一行。这样到最后总结对比不同模型效果时,你不需要重新跑代码,直接看日志就能写论文的“模型对比”部分。这个习惯在备赛阶段尤其有用,因为你会试很多模型,只靠记忆非常容易混。

4. 论文怎么写才能让评委愿意给高分

4.1 摘要才是真正的战场

竞争激烈的时候,评委看每篇论文的时间可能只有几分钟,摘要读得细一点,正文基本是扫读。所以我会把摘要当作一篇独立文章来写,核心句式是“针对某问题,建立了某模型,采用某方法求解,得到某结果”。这不是空话套话,而是信息密度极高的结构:要在四句话内交代清楚问题、方法、结果和亮点。

具体做法:先写完正文所有模型和图表后,再回头写摘要。因为只有结果确定了才知道摘要该强调什么。可真有人第一天就写好摘要,最后改来改去,前后一致性出问题。摘要里出现的每一个数字,我都要求必须能在正文里找到对应出处,否则就是对不上号。

4.2 模型假设要写得“少而精”

模型假设经常被当成充数内容,其实它会直接影响评委对模型成立范围的判断。好的假设是:每一条都对应一个数学简化或数据前提,比如“不考虑突发中断”“数据缺失的时段用插值替代”。如果假设太多,说明模型脱离实际;如果太少,论文逻辑又立不住。我一般控制在5到8条,并保证每条假设都能在模型建立部分被用到。

写假设时还有一个容易忽略的点:假设和后续模型要形成呼应。例如你假设“某参数在短期预测中保持不变”,那后面的模型里就不要出现这个参数的动态方程;如果你假设“噪声服从正态分布”,那你在误差分析里就不能只用绝对值误差,而要配合方差分析。假设不是摆设,而是模型的边界。

4.3 公式、变量表和图表编号,统一提前做

很多队伍写到一半就乱套,公式没有编号,图表下文跳来跳去。我的建议是,论文写作时先搭一个空模板,把一级标题、二级标题、图表占位符、公式编号段都建好,再逐块填充。写公式用LaTeX,变量符号与之前提到的“变量表”保持一致。同一个符号绝对不能一会儿是D、一会儿是d,这种错误虽然小,但被评委看到很影响观感。

公式编号也有技巧:不要等写完再统一编号,因为那样很容易漏。最好每写一条公式就顺手编号,比如(1)、(2)、(3)。正文里引用公式时养成习惯,写成“由式(3)可得”,而不是“由上式可知”。图表同理,所有表格和图片都要有编号和标题,且在正文里至少被提到一次。没有被提到的图表,要么删掉,要么补充说明。

4.4 附录不是代码垃圾场

很多队伍喜欢把全部代码原封不动贴进附录,导致附录比正文还长。我会把代码精简成核心函数片段,按数据预处理、模型求解、可视化分三块放。其他重复代码不贴。如果代码超过两三百行,就放到说明“完整代码已提供”并送文件附件,而不打印到论文里。这个做法让论文看起来干净,评审体验好,分数自然会高一些。

附录里还可以放一些“备查”内容,比如补充的灵敏度实验图、不同初始值下的收敛情况、变量基础统计表。这些内容放正文会显得冗长,但放附录又能证明你们的工作量。要注意的是,附录不是为了凑页数,而是为了让正文更聚焦。如果评委压根不会翻到附录,那核心结果一定要出现在正文里。

5. 我踩过的坑:时间、验算、团队协作

5.1 时间分配定型:前紧后松才是正常节奏

我见过太多次前三天磨模型、最后一天狂赶论文的戏码。那基本意味着模型再漂亮,写出来也像一个草稿。比较舒服的安排是:第一天结束前完成数据预览和问题拆解,第二天到第三天中午完成主体建模和初步结果,第三天下午优化并留出时间画图,第四天上午统一论文,下午做摘要和核查。

这不一定适合所有队伍,但核心逻辑一样:写论文的时间不可压缩,因为摘要的质量决定上限。如果队伍里有人写作能力比较强,他最好从第一天就开始整理“问题描述”和“模型假设”,而不是等到结果出来才动笔。这样最后一天的统稿压力会小很多。

5.2 验算不是可选项,而是必选项

每次改完参数或换了数据,都需要重新跑一遍完整流程,确认结果没崩。这种崩不一定报错,可能是某个数值突然变成负数,或者某个拟合度突然下降。我有个习惯:每改一次代码,就输出一次关键指标对比,存成一个小日志文件。这样答辩被问“你们调过哪些参数”时,可以直接拿出数据,而不是凭记忆。

有一个特别需要关注的场景:当你“手滑”改错了一个常量,比如把单位万写成了千,整个结果都会系统性偏移,而且可能并不报错。这种错误只能靠验算才能发现。验算不一定要用复杂数学方法,最简单的做法是“把某个极端场景代入模型,看结果是否符合直觉”。比如模型在某个参数为零时,理论上输出也应该是零,如果结果不符合,代码里一定有问题。

5.3 团队协作的分工与统稿

常见的分工是一个建模、一个编程、一个写论文,但这样容易让写论文的人不完全懂模型细节。更好的做法是边做边同步:建模的人写好模型说明,编程的人写好代码注释,写论文的人负责把它们串成完整故事。最后一天必须有人统稿,统稿者有权删除冗余段落,统一所有符号和措辞。这个角色往往由经验最多的人担任,不一定是队长。

还有一个容易被忽略的协作问题:文件版本。比赛最后一天经常出现“我改过的代码去哪儿了”的混乱。所以我建议从一开始就用带日期和版本号的命名方式,比如“model_v3_0719.py”,或者用同步盘,避免用“最终版”“最终版2”这种命名。别小看这个细节,很多团队最后提交的代码和论文里图表对不上,就是因为版本混乱。

每次比赛结束我都会把代码和论文存档,标注“哪些模块下次可以直接复用”。现在回看,那些真正拿高分的队,未必是最聪明的队,但一定是最稳的队:审题稳、代码稳、论文稳。最后一件事务必记牢:最后两小时不要引入新模型,只做一件事——把摘要逐字读一遍,和正文图表核对所有数字,那两小时的价值往往比之前任何半天都高。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进