☰
数学建模B题破题全攻略:从整数规划到预测模型实战代码解析
2026/10/22 7:57:44 网站建设 项目流程

每年华数杯的B题都有一个共同特点:题目看着像一道"业务题",实际上考察的是你能不能把一段模糊的业务描述快速翻译成清晰的数学对象。2026华数杯的B题也不会跳出这个套路。我带了这么多年参赛队伍,最深的感受是:B题真正的分水岭不在算法多高级,而在破题思路是否清晰、模型选型是否合理、代码能不能在有限时间内跑出可解释的结果。这篇文章我把带队的整套打法拆开讲,覆盖读题破题、模型选型、Python代码实现、灵敏度分析和三天赛程安排,希望正在备赛的同学能直接照着用。

1. 读题阶段就把"决策故事"翻译成"数学对象"

1.1 第一遍读题,只做四件事

拿到B题题面,我从来不让队伍直接开数据。第一步永远是用草稿纸画一张四栏表格,把题干所有信息分门别类扔进去。四栏分别是:决策者是谁、可选动作有哪些、限制条件是什么、要优化的目标是什么。

举例来说,如果今年B题背景是某城市要在若干候选点增设共享出行服务点,预算有限,同时要考虑覆盖居民需求、建设成本、未来使用量预测。那四个栏目大概填成:决策者=城市运营方;可选动作=在哪些候选点建、建多大;限制条件=总预算、服务半径、点与点之间最小间距;优化目标=综合成本最小或覆盖收益最大。题干里那些描写"未来使用量"的段落,本质上是为后面预测子问题做铺垫,不是让你去读出行市场的宏观报告。

这一步的好处是强迫你把题干读透。很多队伍看完题就急着写公式,结果写到第三天发现漏了一个关键约束,返工代价非常大。四栏法十分钟就能完成,但它决定了后面两天半的方向。

1.2 快速判断这道题属于哪个"问题家族"

读完题后,我要求队伍用一张判断表在五分钟内确认题型。数学建模B题看上去千变万化,实际上绝大多数都落在四个家族里:

题干关键词问题家族主模型方向
选址、分配、调度、最低成本、最大收益优化型整数规划、线性规划、图论模型
趋势、未来、预测、需求量预测型时间序列、回归、机器学习
评价、排序、优劣、方案比选评价型熵权法、TOPSIS、层次分析法
多个问法组合出现混合型拆成多个子问题逐个击破

B题特别喜欢出混合型:第一问预测,第二问在预测基础上做优化,第三问对多方案排序。这种结构其实是好消息,因为每个子问题的解法都有成熟套路,难点只在于子问题之间的数据衔接。把题型确认下来之后,工具地图就已经在脑子里展开了。

1.3 假设清单:宁可写得满,不要漏得空

论文评审时,假设部分是评委第一个会看的内容。不少队伍吃了亏——模型里明显用了"服务半径500米"这个条件,却在假设里完全没提,评委只能猜测,这对论文打击很大。

我习惯的做法是,把题目里所有"隐含条件"都提取出来,逐条写成带编号的假设。凡是模型用到但题目没有明确给出的量,都必须出现在假设里。例如"每个服务点覆盖半径统一取500米""建设成本与投资额成正比""居民需求在一段时期内保持稳定"。假设不是写得越少越好,而是边界越清楚越好。你写了"为什么可以这样假设",评委才知道你具备建模的严谨性。

2. 模型选型的底层逻辑:为什么B题绕不开这三套框架

2.1 优化类:先看解空间大小,再定精确算法还是启发式

B题第一问如果涉及资源配置,本质上就是个数学规划问题。我判断模型的第一步是数决策变量的个数。变量十几个,比如候选点就四五个、需求点十几个,用0-1整数规划配一个求解器就能拿到全局最优解。变量一旦上百,精确算法可能跑不动,这时候才轮到遗传算法、模拟退火这类启发式方法上场。

还有一个很多人忽略的原则:不是算法越高级越好。评审最在意的是模型能不能解释、结果能不能复现。一个简单的整数规划模型,配上清晰的灵敏度分析,通常比堆一个复杂的多目标智能优化算法更讨喜。我实赛时经常先做线性松弛版本,看下界长什么样,再恢复整数约束对比结果。这一步本身就能写进论文,作为"模型合理性验证"的素材。

2.2 预测类:先摸数据量,再决定模型复杂度

预测子问题是B题的绝对高频考点。我的经验法则非常简单:数据点少于30个,不要碰深度学习和复杂集成。优先尝试移动平均、指数平滑或灰色预测这类参数少、解释性强的模型。数据量足够大,比如有几个月甚至几年的逐日数据,且题目给了多个外生变量时,再换成多元回归、随机森林或梯度提升。

我给自己总结过一张预测选型表,实战效率很高:

数据情况推荐模型理由
数据短、有指数增长趋势指数平滑、灰色预测参数少,抗噪性强
数据量大、线性关系明显多元线性回归系数可解释,贴近业务直觉
数据量大、非线性强随机森林、梯度提升能自动捕捉交互效应
带空间位置和距离信息距离加权回归直接把地理信息引入模型

选完模型之后,最关键的一步是留一部分数据做验证。很多队伍把全部数据拿去拟合,然后说模型拟合得很好,这就是典型的自欺欺人。B题数据量通常不大,留出20%做验证完全可行,这20%的数据会让你的可信度明显提升。

2.3 评价类:去量纲先做,权重别拍脑袋

第三问往往要求对多个候选方案排序。我的固定组合是熵权法加TOPSIS。先解释为什么不用层次分析法:层次分析法虽然经典,但判断矩阵的主观性太强,评委最爱问的就是"你的权重为什么这么定"。熵权法依据指标的信息量计算权重,至少有一个可量化的客观依据。TOPSIS负责计算各方案与理想解、负理想解的距离,得到贴近度排序,逻辑相当直观。

整个流程中容易翻车的点是指标方向。成本类指标越小越好,要负向化;效益类指标越大越好,要正向化。两道处理必须做在计算权重之前。我的习惯是对每个指标先标注方向,负向指标统一转换成 max(X) - X,再进入熵权法函数。

2.4 混合结构:子问题之间的数据流要接得严丝合缝

一道题里又要求预测又要求优化时,两个模型之间是上下游关系。典型处理是:先用预测模型估计各个候选点的未来使用量,再把这个使用量作为优化模型里的收益系数。

这里的坑在于,预测值不是确定数,它带着误差。直接把点估计值塞进优化模型,会让结果看起来过度精确。我会在优化模型里给预测值乘一个保守系数,比如0.9或者0.85,并在模型中注明这是风险折减。能把这种不确定性写进模型,本身就比很多队伍高一个段位,说明你不是在生硬拼接模型,而是真的思考了数据从上游传到下游时的损耗。

3. 从数学符号到能跑的代码:一道典型B题的完整实现

3.1 虚构题面与建模目标

为了把理论落到实地,我用一道典型的资源配置型B题来走全流程,题目背景是虚构的:某城市有10个居民需求点,4个候选服务点,每个候选点有建设成本,也有它能够覆盖的需求点集合。预算有限,目标是在候选点里选择若干开设,使得被覆盖的居民总数最大。别看规模小,这个模型在骨架层面和那些几十个候选点、几百个需求点的大题完全一致。

定义决策变量:x_j,j=1到4,等于1表示第j个候选点被选中;y_d,d=1到10,表示需求点d是否被覆盖,取0到1之间的连续值就可以。目标是最大化覆盖人数之和。成本约束是选中点的建设成本总和不超过预算。需求点的覆盖逻辑是:如果y_d=1,那么至少有一个能覆盖d的候选点被选中。

3.2 整数规划求解代码:覆盖问题的线性化技巧

下面这段代码用SciPy的milp实现,环境适配简单,不依赖商业求解器。代码规模可以随时放大,几十个候选点、几百个需求点都能扛住。

import numpy as np from scipy.optimize import milp, LinearConstraint, Bounds num_c = 4 # 候选点数量 num_d = 10 # 需求点数量 cost = np.array([80, 60, 55, 40]) # 各候选点建设成本 cover = { 0: [0, 1, 2], 1: [2, 3, 4, 5], 2: [5, 6, 7], 3: [8, 9] } # 每个候选点能覆盖的需求点索引 demand = np.array([120, 90, 150, 80, 110, 200, 70, 130, 100, 140]) budget = 120 # 目标:最大化覆盖人数,等价于最小化负覆盖人数 c = np.zeros(num_c + num_d) for d in range(num_d): c[num_c + d] = -demand[d] # 成本约束,形式:cost_j * x_j 之和 <= budget constraints = [] A_cost = np.zeros(num_c + num_d) A_cost[:num_c] = cost constraints.append(LinearConstraint(A_cost, -np.inf, budget)) # 覆盖逻辑约束:y_d <= 所有覆盖d的候选点选中数量之和 for d in range(num_d): row = np.zeros(num_c + num_d) for j in range(num_c): if d in cover[j]: row[j] = 1.0 row[num_c + d] = -1.0 constraints.append(LinearConstraint(row, 0.0, np.inf)) integrality = np.concatenate([np.ones(num_c), np.zeros(num_d)]) bounds = Bounds(np.zeros(num_c + num_d), np.ones(num_c + num_d)) res = milp(c, integrality=integrality, bounds=bounds, constraints=constraints) print("最大覆盖人数:", int(-res.fun)) print("选中的候选点:", [j for j in range(num_c) if res.x[j] > 0.5]) print("被覆盖的需求点:", [d for d in range(num_d) if res.x[num_c + d] > 0.5])

这里有两个值得记住的建模技巧。

第一,目标函数为什么取负:求解器默认做最小化,我要最大化覆盖人数,所以只能给目标系数全部取负,把最大化变成最小化负值。

第二,覆盖约束的线性化:要让"y_d只能在没有一个覆盖它的点被选中时取0"这个逻辑成立,我把它写成y_d - 所有覆盖它的x_j之和 <= 0。因为目标函数里y_d带着负的收益系数,求解器在条件允许时一定会把y_d推到1。这个写法绕开了逻辑或运算,是覆盖类优化问题最常用的建模手法,强烈建议提前默写下来。

3.3 预测子问题的基线代码骨架

如果题面里还带着"预测各候选点未来使用量"的小问,我先跑一个简单版本作为基准模型,再跑增强模型做对比。以移动平均加趋势修正为例:

import pandas as pd import numpy as np # 历史周使用量序列,实际比赛时换成读附件数据 data = pd.Series([55, 58, 62, 60, 66, 72, 75, 78, 82, 86]) window = 3 base = data.rolling(window=window).mean().iloc[-1] trend = (data.iloc[-1] - data.iloc[-window]) / window pred_next = base + trend print("下一期基线预测值:", pred_next)

这个模型的优点是完全能解释:基线上是最近三期的均值,趋势修正是最近一期与三期前位置相比的单期变化量。放到论文里非常顺。

如果数据量大、特征多,我会在同样的基线上换成随机森林回归,把天气、周边业态、人口密度等字段塞进去。论文里必须写清楚:基准模型解决什么问题,增强模型改进了什么,两边的误差指标分别是多少。用对比来证明你的选择,而不是空口说"我的模型精度高"。

3.4 多方案评价里的熵权TOPSIS代码

第三问对候选选址方案排序时,熵权加TOPSIS是性价比最高的组合。直接看代码:

import numpy as np def entropy_weight(X): # X 是 [方案数, 指标数] 的矩阵,所有指标已统一为正向 X = np.asarray(X, dtype=float) P = X / X.sum(axis=0, keepdims=True) k = 1.0 / np.log(X.shape[0]) e = -k * np.sum(P * np.log(P + 1e-10), axis=0) d = 1 - e w = d / d.sum() return w def topsis(X, w): X = np.asarray(X, dtype=float) norm = X / np.sqrt((X ** 2).sum(axis=0)) V = norm * w ideal = V.max(axis=0) neg_ideal = V.min(axis=0) d_pos = np.sqrt(((V - ideal) ** 2).sum(axis=1)) d_neg = np.sqrt(((V - neg_ideal) ** 2).sum(axis=1)) return d_neg / (d_pos + d_neg) # 示例:5个方案,4个正向化指标 scores = np.array([ [3500, 92, 85, 40], [4200, 88, 90, 35], [3000, 95, 75, 48], [3800, 90, 88, 42], [4500, 84, 92, 30] ]) weights = entropy_weight(scores) print("熵权法权重:", weights) print("TOPSIS贴近度:", topsis(scores, weights))

写这段代码最容易犯的错是忘记把指标统一方向。成本类指标不经过负向化处理就放进熵权法,计算出来的权重会误导排序。我的习惯是先建一个带"方向标识"的指标表,所有负向指标做一次max - X转换,再喂给函数。

3.5 结果可视化:图是为了让评委快速信任你

B题论文里,图和表是评委获取信息效率最高的入口。我最常画三张图:选址方案的覆盖关系图、预测结果对比图、TOPSIS排名条形图。以覆盖关系图为例:

import matplotlib.pyplot as plt c_xy = np.array([[1, 1], [2, 3], [4, 2], [5, 4]]) d_xy = np.array([[1.2, 1.1], [1.9, 3.2], [3.0, 1.5], [4.5, 2.3], [5.1, 4.2], [2.5, 2.0], [3.7, 3.8], [4.2, 1.2], [5.5, 3.0], [0.8, 2.2]]) plt.figure(figsize=(6, 4)) plt.scatter(d_xy[:, 0], d_xy[:, 1], c='k', marker='o', label='需求点') selected = [0, 1] plt.scatter(c_xy[selected, 0], c_xy[selected, 1], c='r', marker='^', s=120, label='选中候选点') candidate = list(set(range(4)) - set(selected)) plt.scatter(c_xy[candidate, 0], c_xy[candidate, 1], c='gray', marker='^', s=80, label='未选候选点') plt.legend() plt.axis('equal') plt.title('选址方案覆盖示意') plt.show()

图不需要做得多炫,核心是让评委一眼看出你选了哪些点、覆盖了哪些区域。如果论文面向中文评审,标题记得设置中文字体,否则图里会出现方框乱码,这种细节很影响阅读体验。

4. 灵敏度分析与结果验证:让模型从"能跑"变成"可信"

4.1 为什么灵敏度分析是必选项

B题里的参数,很多都是估计值,比如预算、成本、覆盖半径。评委几乎必问的一个问题是:这些参数稍微变化时,你的方案还稳吗?如果模型对某个参数极其敏感,参数小幅波动方案就完全变掉,那这个模型的实际价值就打折扣了。所以灵敏度分析不是"写完论文后的加分项",而是"建模流程里本来就该有的一环"。

4.2 一套省时的参数扰动实验流程

我的固定做法是把核心参数从负20%到正20%扫五档。以刚才的选址题为例,把预算分别设为96、108、120、132、144,对每个值重新求解,记录最优方案覆盖率和目标函数值。然后把覆盖半径也做一遍同样的扫描。

扫完之后画一张"覆盖率随预算变化"的折线图,评委马上能看出模型的稳健性。如果方案在参数变化一档两档内保持不变,最好写;如果变了,就分析是哪条约束开始起作用。比如预算从120降到96时,覆盖面最广但造价最高的那个点被迫让位,这个现象写出来比念数字有说服力得多。

4.3 模型对比实验怎么设计才严谨

想证明"我选的模型比替代方案好",不能只说精度高,要设计两个层面的对比。

第一层,简单基准模型对比。比如预测题里,用"全部取平均值"作为基准,再让你的模型上场。第二层,替代模型对比。比如把整数规划换成贪心算法,或者把随机森林换成多元回归。对比表格按下面这样写:

对比项基准模型本文模型说明
覆盖率0.720.86覆盖率提升明显
求解时间(秒)0.312.5求解更精细,时间可接受
预算±20%扰动下方案是否稳定方案漂移保持稳定稳健性更强
预测误差MAE(仅预测子题)8.75.2误差下降40%

对比时要注意,求解时间如果差太多,你得主动解释是精细求解的代价,不是算法劣势;精度提升如果依赖过度拟合,也要如实承认,并说明在验证集上的表现仍然不错。

4.4 误差和残差的解释比指标本身更重要

预测子题里,把MSE、MAE、R方这些指标算出来只是第一步。评委想看的是,你对残差有没有洞察。我的习惯是画残差图,如果残差在固定时间周期系统性偏高,说明模型漏掉了周期性或外部变量。论文里写一句"残差在周末时段存在系统性偏大,后续可在特征中加入节假日哑变量缓解",比任何空洞的"模型精度较高"都更有说服力。

5. 三天赛程怎么排、常见丢分点怎么防

5.1 前八小时定生死

我带队的节奏非常固定:第一天上午只做读题、列对象、搭模型框架,下午把核心代码跑通。很多队伍把简单问题复杂化,第一天晚上代码还没跑起来,第二天开始焦虑,第三天直接崩溃。如果第一天晚上核心代码还出不了第一版结果,说明模型选复杂了,赶紧简化。

整体时间节奏建议:

  • 第一天上午:读题、写假设、确定模型框架。
  • 第一天下午:编写核心求解代码,产出第一张结果表。
  • 第一天晚上:跑灵敏度分析,补一个对比模型。
  • 第二天全天:写作、画图、排版同步推进。
  • 第三天:全文校对、摘要打磨、逐段检查代码可复现性。

5.2 五个最容易被扣分的地方

我每年赛后给队伍复盘,发现丢分点高度集中在五个地方。

  1. 没有写清假设,模型一上来就是公式,评委不知道边界在哪。
  2. 数据表字段没说明含义,变量符号和题面对应不上。
  3. 只给最终结果,不给求解器输出、中间步骤或关键代码片段。
  4. 图和表没有标题、没有单位、没有必要的图例注释。
  5. 附录代码跑不通,或者代码结果和正文表格对不上。

前四个属于表达问题,第五个是硬伤。防止第五点的最有效办法是论文写作和代码同步推进:每写完一段结果,马上把对应代码存进附录并重新运行,确保数字是从代码里复制出来的,而不是手敲的。

5.3 代码跑不出来时的三套急救预案

数学建模比赛里几乎没有队伍不遇到程序崩掉的情况。我按顺序执行三招。

第一招,缩小问题规模。把候选点从200个降到20个,先确认模型逻辑正确,再逐步放大。大多数"跑不动"都是数据规模直接把求解器压垮了。

第二招,换算法。精确求解扛不住就上遗传算法、模拟退火或贪心加局部搜索。论文里只要写清楚启发式参数的设定和收敛曲线,评委是可以接受的。

第三招,给问题降级。把整数规划松弛成线性规划,先得出一个近似结果作为参考。这比交白卷强,而且可以用来做模型解空间的上下界讨论。

5.4 自建一个"模型-代码-论文片段"经验库

我现在打比赛很少翻参考书,靠的是自建的经验库。每次做完一道题,把题面类型、所用模型、核心代码、论文里的标准表述摘出来,存成几行笔记。下次遇到类似问法时直接检索同类题,框架和代码都可以快速复用,省下来的时间全花在这个题特有的数据特征分析上。

坚持做两三场比赛之后,破题速度会有一个明显的跃迁。

最后说点个人体会。B题没有标准答案模板,但整套方法论是可以练出来的。那些拿题三小时就能出初版结果的队伍,多半不是突然变聪明,而是把读题、模型映射、代码骨架、验证流程都做成了肌肉记忆。下一场比赛里,希望你也能把这套思路变成自己的拿分项。

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

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

立即咨询