简介:运筹优化与数学建模是解决复杂资源配置问题的核心方法,其基本原理是通过定义决策变量、构建目标函数与约束条件,在有限资源下寻找最优方案。这类技术广泛应用于物流选址、库存管理、城市服务设施规划等场景,尤其在需求动态变化的现实问题中,静态模型往往难以刻画真实规律,需要引入多周期、多目标及不确定性建模思路。共享充电宝投放配置正是典型的选址-定容-调度联合优化问题,既要考虑站点覆盖与成本约束,又要应对借还流动和时间潮汐效应。本文从整数规划、排队论、遗传算法等基础方法出发,系统拆解需求预测、数据构造、模型层次递进与算法实现,帮助竞赛选手快速建立从静态选址到多目标进化优化的完整解题框架,提升论文的建模深度与说服力。 每年认证杯的D题,基本都是和“现实场景优化”强相关的综合题,今年很接地气地选了共享充电宝投放。选这道题的同学,我先说个结论:这题想拿省奖不难,想冲国奖必须在“需求刻画”和“联合优化”上做扎实。很多队伍上来就堆遗传算法、堆NSGA-II,结果目标函数定义得稀烂,数据全是拍脑袋的,评委一眼就能看出问题。这篇我把完整题解思路、数据构造方法、核心代码框架、论文组织逻辑全部拆开讲,用的是我自己带竞赛队伍时验证过的方案,拿到题不知道怎么下手的,直接照着这个框架走。
1. 破题视角:这道D题真正在考察什么
1.1 从关键词拆解题目意图
题目核心是“共享充电宝的投放配置”,拆开看有三个关键词:投放、配置、共享充电宝。前两个词指向的是运筹优化问题,第三个词决定了业务约束。
- 投放:本质是选址问题,要在哪些站点放设备,这是一个 0-1 决策。
- 配置:每个站点放多少台充电宝、多少个格口,这是整数规划里的定容问题。
- 共享充电宝:有明显的时间潮汐效应和空间人流分布,不是传统选址那种“固定需求”就能糊弄过去的。
所以题目真正考的是选址-定容-调度联合优化,而不只是简单的整数规划。很多队伍把问题简化成“在几个候选点里选几个”,然后算一个最大覆盖模型,这种思路只能拿基础分。因为充电宝的特点是可流动、可循环、需求随时间变化,一个点位的充电宝可能晚上被借走,早上才还回来,所以配置方案必须考虑时间维度,这就是进阶分的来源。
1.2 与经典数学建模题型的映射关系
这道题本质上是一道“选址-分配问题”,在交通运输、物流规划领域非常常见。但共享充电宝有它的特殊性:
| 经典选址问题 | 共享充电宝投放问题 |
|---|---|
| 设施一经建立就固定服务 | 充电宝会随借还流动 |
| 需求基本静态 | 需求有时段波动、有潮汐 |
| 目标通常是成本最小或覆盖最大 | 要同时兼顾盈利、覆盖、服务率 |
| 容量约束相对宽松 | 站点的格口数量高度绑定设备成本 |
映射到数学模型上,就是集合覆盖模型(LSCP)的变体加上库存管理中的容量决策,再叠加上时间维度后变成一个多周期优化问题。在竞赛中,最稳妥的组合是:第一问用确定性模型做静态选址定容,第二问引入时间周期做动态调度与补充,第三问讨论不确定性(需求波动、设备损坏)并做鲁棒优化。
这样的层层递进,正好覆盖了题目的全部要求,也完美对应了评委喜欢的解题节奏,从简单到复杂,从静态到动态,从确定性到不确定性。
1.3 常见的三种“死法”与应对
带竞赛这几年,见过太多队伍在这道题上翻车,方式惊人地一致。提前说清楚,你们别踩。
第一种死法:把数据当成摆设,直接空想参数。有些组一上来就假设“每个站点需求为100”,然后开始跑模型,完全不做数据预处理和可视化分析。这种论文的模型就算再漂亮,评委也会问一句:你的参数哪里来的?有依据吗?
第二种死法:试图把问题全盘动态化,复杂度失控。某个队写了几十个变量、几十个约束,把所有能想到的因素都塞进模型,结果求解器跑了一天都不收敛,最后只能随机生成一个可行解交上去。
第三种死法:多目标处理粗糙。看到有“成本最小化”和“覆盖率最大化”两个目标,就直接线性加权,权重拍脑袋定成 0.5 和 0.5,没有做灵敏度分析。我负责任地说,这种处理在评阅时大概率会被划到“建模一般”档次。
应对方式其实很清晰:先做简化但严谨的静态模型,再逐层进阶;数据必须有依据,哪怕是仿真数据也必须说清楚生成逻辑;多目标问题用 Pareto 前沿来展示权衡关系,而不是给一个权重完事。这篇博文后面的所有内容,都是围绕这三条避坑经验展开的。
2. 建模基座:共享充电宝投放的变量设计与约束表达
2.1 决策变量怎么定义才不绕弯
决策变量的设计决定了整个模型的复杂度。看到题目后,很多人第一反应是定义x_ij表示“第 i 个区域投放第 j 种型号的充电宝数量”,这么定义本身没错,但会立刻把问题推向一个大规模整数规划——如果候选点是 50 个、型号是 3 种、时间段是 24 个,那变量接近 3600 个,再加上约束,求解器会非常吃力。
我建议的变量分层设计方法是这样:
第一层是选址变量,y_j表示候选点 j 是否建设共享充电宝站点,取值 0 或 1,这个变量用于决定“在哪投放”。
第二层是容量变量,c_j表示站点 j 的格口总数,也就是最大容量,这个变量用于决定“放多少设备”。
第三层是分配变量,x_{j,t}表示站点 j 在时段 t 的实际充电宝在架数量,这个变量用于决定“配合动态调度时怎么分配”。
第四层是调度变量,z_{j,t}表示站点 j 在时段 t 需要补充或者调拨的充电宝数量,用于刻画站点之间的协调。
这样把变量拆成“选址-容量-在架-调度”四个层次,每一个决策变量都有明确的业务含义,既不会重复,也不会漏定义。而且最妙的是,这个分层方式天然适合分问求解:第一问用 y 和 c,第二问加入 x 和 z,第三问引入不确定性参数。
2.2 目标函数:赚钱、覆盖率、服务率的多目标权衡
共享充电宝投放的目标绝对不是单一的,我梳理出三个核心目标,分别对应不同利益相关方的诉求:
目标一:运营利润最大化。这与企业的核心诉求直接相关,涉及到收入减去成本的净值。这个目标的表达式为:
max Profit = Σ_t Σ_j (p × r_{j,t} - h × u_{j,t} - f × y_j)
其中,p 表示单次借出的收益,r_{j,t} 表示站点 j 在时段 t 的实际租借次数;h 表示单台充电宝的单位持有成本,u_{j,t} 表示在架但未被租出的设备数;f 表示站点固定运营成本。这个目标综合反映了利润的三方面:租借收入、设备闲置损耗和站点固定支出。
目标二:需求覆盖率最大化。这个目标的权重在于衡量所有用户需求中被满足的比例。假设总需求量为 D_total,实际满足的需求量是 ΣD_served,则覆盖率等于 ΣD_served / D_total。覆盖率过低意味着大量用户借不到充电宝,非常影响用户体验和站点口碑。
目标三:用户服务率最大化。注意这里的服务率和覆盖率有区别,覆盖率是某一个时段内总体满足的比例,而服务率要求每个时段的供需比不低于一定的阈值,比如r_{j,t} / d_{j,t} ≥ α,α 通常取 0.8 或 0.9。这个约束刻画的是“高峰期不能说断就断”的底线要求。
三个目标之间天然冲突:你要利润率最高,那就在热门区域疯狂投放、对冷门区域直接放弃,结果覆盖率难看;你要覆盖率最大,就会在冷门区域也塞很多设备,结果设备使用率低、亏损严重。所以这就是一个典型的多目标优化问题。
处理多目标的方法,我强烈建议不要直接线性加权,而是用ε-约束法或者NSGA-II生成 Pareto 前沿,让决策者在利润和覆盖率之间做权衡。
2.3 约束条件:预算、格口容量、服务半径的数学化写法
构建模型时,我结合共享充电宝的实际运营情况,梳理了几个必不可少的约束条件,每一项都有实际的物理含义:
预算约束:Σ_j (c_j × g + y_j × s) ≤ B,即建设成本加上设备采购成本不能超过总预算。这里 g 表示单格口设备成本,s 表示站点建设固定费用。这个约束是硬性约束,决定了投放总量的上限。很多队伍会忽略这个约束,导致投放数量严重超出实际合理范围,这是致命伤。
格口容量约束:c_j^{min} ≤ c_j ≤ c_j^{max},且x_{j,t} ≤ c_j,即每个站点的格口总数有上下界,同时在架设备数不能超过格口总数。下界是为了保证站点具备基本服务能力,上界则反映了场地空间和成本的限制。
服务半径约束:d_{ij} × y_j ≤ R,表示任意需求点 i 到被选择服务站点 j 的步行距离不得大于服务半径 R。这个约束是选址模型的灵魂,直接影响用户能否方便地借到充电宝。建议 R 取值为 300 到 500 米,共享充电宝的核心特征就是应急性和便捷性,服务半径超过这个范围就会导致订单的大量流失。
供需平衡约束:Σ_j x_{j,t} ≥ λ_t × D_t,即各站点在架设备总量不小于时段 t 总需求量的 λ_t 比例。这个约束防止出现“每个点位都不缺设备、但总需求和总供给整体不匹配”的情况。
这些约束写得越贴近业务实际,评委就越认可。但也要注意约束数量别太多,否则模型求解难度急剧上升,后面我会讲怎么化简。
2.4 需求不确定性的处理
共享充电宝需求的最大特征就是波动性:工作日午休时间、周末晚上、商场活动期间,需求完全不是一个量级。处理不确定性主要有三种思路,分别适用于不同场景:
思路一:随机场景法。根据历史数据生成多种需求场景,每个场景有概率 p_i,然后把目标函数改成期望形式,max E[Profit] = Σ_i p_i × Profit_i。这种方法的缺点是场景数一多,模型规模快速膨胀。
思路二:鲁棒优化法。不求每个场景都最优,而是让方案在最坏情况下也不差。假设需求在区间[D_min, D_max]内波动,用Γ来控制保守程度,构建一个对需求扰动的鲁棒对等模型。这种方法特别适合比赛,因为评审专家非常看重模型对极端情况的应对能力。
思路三:机会约束规划。P(Σx ≥ D_t) ≥ 1 - ε,表示需求满足的概率不低于 95% 或 99%。这是最直观的表达方式,但在某些求解器里不好直接处理,需要转换成确定性等价形式。
在比赛实际操作中,我强烈建议:第一问明确用确定性需求,第二问问“需求随时间和地点波动”的时候,再切换成随机场景法或鲁棒优化。这样层次清清楚楚,不会一上来就把自己卡死在求解复杂度上。
3. 数据准备:仿真数据生成与需求预测的完整链路
3.1 数据集的构成与预处理
正式的竞赛题一般会给一些数据,比如商圈位置、人流量统计、人口分布、已有充电宝站点位置等。但今年的 D 题如果给的是仿真数据或者只有稀疏的几个表格,那就需要自己补数据。关键是补出来的数据必须有逻辑可循,不能用random函数一把梭。
我的做法是构建一张城市区域网格图,每个网格有这些属性:
| 字段名 | 含义 | 数据类型 |
|---|---|---|
| grid_id | 网格编号 | int |
| lon, lat | 中心点坐标 | float |
| poi_density | 兴趣点密度(餐饮/商场/写字楼/地铁) | float |
| flow_peak | 人流高峰标识(商务区/住宅区/景区) | category |
| base_demand | 基础需求基数 | float |
| weekday_factor | 工作日/周末需求系数 | float |
| hour_factor | 24小时时段系数 | list[24] |
| competitor_num | 已有竞品点位数量 | int |
基础需求的生成不能瞎拍,我用的是经验公式:
base_demand = (poi_density × α + flow_signal × β + competitor_suppress × γ) × population_density
其中competitor_suppress表示竞品对需求的抑制作用,因为竞品越多,属于你的需求就越少。归一化后,用它乘以城市总需求估计值,就可以得到每个网格的日均基础需求。
3.2 特征工程:POI、人流、时间、竞品
拿到原始数据后,真正决定模型上限的反而是特征工程。在共享充电宝场景里,有几个特征必须构造,而且这些特征很可能是从原始数据里直接提取不出来的:
POI 加权密度。不能只看“有多少个 POI”,要看是什么类型的 POI。餐饮、商场、电影院、地铁口的充电需求完全不同。我一般用熵权法或者简单打分法给每种 POI 一个权重:地铁口 0.4、餐饮 0.3、购物 0.2、住宅 0.1,然后算加权密度。这个加权密度是需求预测最重要的特征之一。
时间槽特征。基于 24 小时划分 8 个时间槽(03:00-07:00、07:00-10:00、10:00-13:00、13:00-17:00、17:00-20:00、20:00-23:00、23:00-03:00)。不同区域的时段系数差异非常大,商务区在 10 点前有早高峰、商场在 18-21 点达到峰值、住宅区则是 19 点以后的需求最旺盛。
竞品饱和度。某区域周围已经有 30 台其他品牌的充电宝,你在这里再投 10 台,利用率一定比在 3 台竞品的地方低。用competitor_num / grid_area表示竞争强度,这个特征对“投放多少”的约束影响显著。
天气与突发事件系数。如果题目给了天气数据,可以构造“雨天需求上浮系数”。充电宝在雨天、恶劣天气下需求会明显提升,这个系数取 1.2 到 1.5 之间都是合理的。
这些特征做好之后,需求预测就有了抓手。否则就是无源之水、无论证根基。
3.3 需求预测模型对比
做好特征后,接下来需要预测每个区域每个时段的需求量。有三个层次的方法:
层次一:时间序列外推。用历史平均值、指数平滑、Holt-Winters 方法。优点是简单快速,缺点是完全没有利用空间特征,精度一般。
层次二:机器学习回归。XGBoost 和 LightGBM 在这个场景下效果都不错。输入特征包括网格特征、时段编码、星期类型、节假日标记,输出是需求数量。我自己测试下来的经验是:加上了空间特征之后,预测误差相比纯时间序列可以下降 25% 到 30% 左右。具体可参考下面的简单实现:
import xgboost as xgb from sklearn.model_selection import train_test_split # 假设 features 已经是形状为 (n_samples, n_features) 的特征矩阵 # label 是需求数量 X_train, X_test, y_train, y_test = train_test_split( features, demand, test_size=0.2, random_state=42 ) model = xgb.XGBRegressor( n_estimators=300, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42 ) model.fit(X_train, y_train) print("R2:", model.score(X_test, y_test))层次三:深度学习。用 LSTM 或者 Transformer 做时间序列预测,把每个区域的时间序列作为输入。说实话在竞赛场景里,除非数据量特别大,否则 LSTM 的收益不一定比 XGBoost 高,而且调参时间成本极高。不建议作为首选方案。
我的核心建议是:用 XGBoost 作为主力预测器,用网格搜索确定最优参数,用时间序列交叉验证评估模型稳定性。这套方案既能在论文里写清楚,又不会在比赛时间内爆炸,非常稳妥。
4. 核心模型:选址-定容联合优化的完整推导
4.1 基础选址模型:从最大覆盖到容量约束
第一步先把选址模型搭起来。最经典的模型是最大覆盖问题(MCLP)和集合覆盖问题(LSCP)的组合思路,它们的核心是在“尽可能覆盖更多需求”和“控制成本”之间找到平衡。引入容量约束后就变成容量受限的选址问题(CFLP),这在学术上是比较成熟的问题,但放到共享充电宝场景里要做两个调整:
第一个调整是覆盖半径的设定不同。共享充电宝的服务半径不是简单的欧氏距离,要考虑步行可达性。可以用google map的步行距离或者用道路网做最短路径计算。如果数据条件不允许,直接简化成:两个点之间的直线距离乘以绕行系数 1.3 到 1.5,作为估算步行距离。
第二个调整是容量上限不能固定为一个数。商业区、交通枢纽和住宅区的充电宝流转速度完全不同,所以容量上限应该结合该站点的需求预测值来确定,比如c_j^{max} = ceil(1.2 × demand_peak_{j}),即取该站点峰值需求的 1.2 倍作为容量上限,这样既有缓冲空间,又不至于浪费设备采购成本。
4.2 数量配置模型:定容决策的随机化改进
确定哪些位置建站点之后,接下来就需要确定每个站点的充电宝数量和格口数量。很多队伍的答案到这一步就结束了,但这其实是最容易出彩的部分——因为它可以有两种截然不同的模型策略:
策略一:基于排队论的容量配置。把每个站点看成一个多服务台系统,用户到达率 λ_j、平均每次租借时长服从负指数分布(均值 1/μ),借用M/M/C队列模型的稳态概率公式来推导容量 C,使得用户等待概率低于 5%。这个策略的优点是非常有理论深度,论文里写出来评委一眼就能看出建模功底;缺点是公式推导时间较长,且对数据要求高。
策略二:基于动态规划的容量配置。用离散时间马尔可夫链来描述每个站点在架充电宝数的变化,状态转移概率由需求和归还的联合分布决定。最优格口数是使得单位时间总成本(缺货损失 + 设备持有成本)最小的格口数。这个方法有效且完全能求解,在代码实现上也相对可控。
我实际做的时候,是把两种策略结合起来的:先基于需求预测数据的 85 分位数定一个基础容量,再用排队论做校验,调整到在峰值时段用户到达即借到的概率大于等于 90%。这样写论文既有理论支撑,又有数据验证,比单用某一种方法要好得多。
4.3 双层规划与多目标进化算法的引入
如果题目的后续问法是“同时考虑不同区域之间的调度”“充电宝在站点间流动”,就需要用到双层规划:
上层是运营商决策,决定各站点的容量配置方案,目标是总利润最大化或总成本最小化。
下层是用户行为模拟,给定充电宝供给分布,用户会根据距离和是否有货选择租借点。这个下层可以用 Logit 模型表达,即用户在站点 j 租借的概率为:
P_j = exp(-θ × d_ij - γ × unavailability_j) / Σ_k exp(-θ × d_ik - γ × unavailability_k)
其中unavailability_j是站点 j 在对应时段的无货概率。上层调整容量配置,下层根据最新的供需状态重新计算用户选择概率,两层反复迭代逼近最终稳定解。
这种双层模型没法用单纯形法或者分支定界法直接解决,需要用启发式算法。我常用的方案是遗传算法(GA)配合局部搜索,把候选方案编码成染色体,用利润、覆盖率等指标做适应度函数,并用帕累托排序保持解的多样性。
4.4 三个层次模型的适用场景对比
为了让大家不迷路,我整理了一个表,把三个层次的模型、适用场景和求解难度对比出来:
| 模型层次 | 核心逻辑 | 适用场景 | 求解方式 | 预期得分 |
|---|---|---|---|---|
| 基础覆盖模型 | 静态选址 + 定容 | 第一问:已有候选点,选点并定数量 | 整数规划(ortools、pulp) | 保底奖 |
| 随机/排队模型 | 考虑需求波动,容量留有富余 | 第二问:需求随时间变化,需设置动态策略 | 排队论公式 + 仿真 | 进省奖 |
| 双层规划 + 多目标进化 | 站间调度 + 用户选择反馈 | 高阶问:考虑跨区域调度与运营策略优化 | NSGA-II / MOEA/D | 冲国奖 |
这个递进结构是我一直推荐的建模节奏。不要试图一步到位写最高级的模型,而是保证每个问都有模型、有求解、有结论,层层递进,这也是评委最喜欢的论文框架。
5. 算法实现与代码走读
5.1 算法选型逻辑:为什么放弃直接求解整数规划
第一问如果用 pulp 或者 ortools 解整数规划,完全没问题;但到了第二问第三问,问题规模上去以后,直接用scipy.optimize.milp或 CPLEX 就很难在合理时间内得到满意解了。所以需要启发式算法,而启发式算法的选型逻辑很重要。
遗传算法的核心优点有两个:一是天然支持离散决策变量(选址 0-1 变量、整数容量变量);二是适应度函数可以是任意复杂的函数,不用担心非凸、不可导等问题。这两个优点恰好命中了共享充电宝投放配置的建模特点。所以我首推遗传算法作为主力求解框架,模拟退火作为局部搜索增强。
5.2 遗传算法核心代码框架
下面这段代码是遗传算法求解选址-定容问题的核心骨架,可以直接改写成题目需要的版本:
import numpy as np from deap import base, creator, tools, algorithms # 定义个体:染色体前 50 位为选址变量(0/1),后 50 位为容量变量(整数) N_POINTS = 50 POP_SIZE = 100 NGEN = 200 creator.create("FitnessMulti", base.Fitness, weights=(1.0, 1.0)) creator.create("Individual", list, fitness=creator.FitnessMulti) def eval_func(individual): # 前50位选点,后50位容量 selected = np.array(individual[:N_POINTS]) > 0.5 capacity = np.array([int(x) for x in individual[N_POINTS:]]) if capacity[selected].sum() > BUDGET_MAX: return (-1e6, -1e6) profit = cal_profit(selected, capacity) coverage = cal_coverage(selected, capacity) return (profit, coverage) def mutate_individual(individual, indpb=0.05): for i in range(len(individual)): if np.random.random() < indpb: if i < N_POINTS: individual[i] = 1 - individual[i] else: individual[i] = np.random.randint(5, 30) return individual, toolbox = base.Toolbox() toolbox.register("attr_bool", np.random.randint, 0, 2) toolbox.register("attr_int", np.random.randint, 5, 30) toolbox.register("individual", tools.initCycle, creator.Individual, (toolbox.attr_bool, ) * N_POINTS + (toolbox.attr_int, ) * N_POINTS, n=1) toolbox.register("population", tools.initRepeat, list, toolbox.individual) toolbox.register("evaluate", eval_func) toolbox.register("mate", tools.cxTwoPoint) toolbox.register("mutate", mutate_individual) toolbox.register("select", tools.selNSGA2) pop = toolbox.population(n=POP_SIZE) algorithms.eaMuPlusLambda(pop, toolbox, mu=POP_SIZE, lambda_=POP_SIZE, cxpb=0.7, mutpb=0.3, ngen=NGEN, verbose=True)这段代码里有两个容易踩坑的细节值得特别强调:
第一个坑:容量变量初始化范围不能太小也不能太大,如果初始范围是 0 到 10,那么最终解的多样性就会很差,很难跳出局部最优。我一般按照需求预测的 30 分位数到 120 分位数来设定初始范围,让算法在合理区间内搜索。
第二个坑:变异算子的设计很关键,对于离散的选址变量直接取反就行,但对于容量变量,应该用random.randint(min_c, max_c)重新采样,而不是加一个高斯噪声然后取整。因为高斯噪声容易让数据逐渐趋同,无法有效探索全局,而且取值还容易出现负值,导致非法个体。
如果你对 DEAP 不太熟悉,也不用慌,直接换成pymoo库也可以,pymoo的 NSGA-II 封装更友好,代码如下:
from pymoo.algorithms.moo.nsga2 import NSGA2 from pymoo.optimize import minimize from pymoo.core.problem import Problem class ChargerProblem(Problem): def __init__(self): super().__init__(n_var=100, n_obj=2, n_constr=1, xl=np.array([0]*50 + [5]*50), xu=np.array([1]*50 + [30]*50)) def _evaluate(self, x, out, *args, **kwargs): # 计算利润和覆盖率,返回给 out["F"] 和 out["G"] profit = np.array([cal_profit(xi[:50], xi[50:]) for xi in x]) coverage = np.array([cal_coverage(xi[:50], xi[50:]) for xi in x]) budget_violation = np.array([max(0, cal_cost(xi) - BUDGET_MAX) for xi in x]) out["F"] = np.column_stack([-profit, -coverage]) out["G"] = budget_violation problem = ChargerProblem() algorithm = NSGA2(pop_size=100) res = minimize(problem, algorithm, ("n_gen", 200), verbose=True)不管用哪个库,核心都是把目标函数写清楚、把约束写清楚、给足够的迭代代数。建议每次跑完保存帕累托前沿的所有非支配解,而不是只取适应度最高的那个,这样分析的时候可以看到多种方案的取舍。
5.3 模拟退火与局部搜索的配合
遗传算法擅长全局探索,但局部精细搜索能力稍弱,在容量微调这种问题上容易不够精准。所以我的完整方案是“遗传算法先跑全局,模拟退火再做局部精修”。
模拟退火的核心思路非常简单,每一次迭代中对当前解做一个微小的扰动(比如某个站点的容量加一、某个冷门站点容量减一),如果新解更优就接受,如果更差就以一定的概率接受,这个概率随温度下降而降低。下面是我用的核心代码片段:
def simulated_annealing(init_solution, T_init=100.0, T_min=1e-3, alpha=0.995, max_iter=500): current = init_solution.copy() best = current.copy() T = T_init for i in range(max_iter): # 微扰:随机选一个点,容量加减1 new_sol = current.copy() idx = np.random.randint(N_POINTS, 2 * N_POINTS) delta = np.random.choice([-1, 1]) new_sol[idx] = np.clip(new_sol[idx] + delta, 5, 30) delta_fitness = evaluate(new_sol) - evaluate(current) if delta_fitness > 0 or np.random.rand() < np.exp(delta_fitness / T): current = new_sol.copy() if evaluate(current) > evaluate(best): best = current.copy() T = T * alpha return best注意,这个模拟退火不是一个独立的解法,它的价值在于“在遗传算法已经收敛到大致的优秀区域后,再做最后的精细化打磨”。两者配合,往往能在有限时间内让最终方案再提升 3% 到 8% 的利润,这个提升在排名上很可能是决定性的。
5.4 求解器的选型建议与效率对比
虽然启发式算法是主力,但第一问完全可以直接用求解器得到最优解,这样论文里既有精确解又有启发式解,对比更充分。
| 工具 | 适用类型 | 优点 | 缺点 |
|---|---|---|---|
| PuLP | 纯线性/整数规划 | 语法简单,适合快速建模 | 大规模问题效率一般 |
| OR-Tools | 整数规划 + 约束规划 | 谷歌出品,CP-SAT 求解器性能强劲 | 要学习特定模型语法 |
| Gurobi / CPLEX | 大规模 MILP / QP | 工业级,速度最快 | 学术免费但需申请,商业版昂贵 |
| scipy.optimize | 非线性规划 | 简单直接 | 对整数和 0-1 支持弱 |
实战经验是:第一问的单点选址用 OR-Tools 的 CP-SAT 求解器,几分钟内就能得到不错的解;第二问第三问的组合用遗传算法。这样既能在论文里展示精确解的质量,也能合理说明大规模问题的求解难度,给启发式算法的引入铺垫。
6. 论文组织与获奖技巧
6.1 摘要写法与评委评分点
数学建模论文的摘要几乎是决定能否拿高分的核心要素。评委往往先看摘要,摘要不清晰,正文再精彩也可能被划档。写摘要时要回答清楚四个问题:
- 问题重述:用两句话简洁概括题目要求,不要照抄题目原文。
- 建模思路:你们把问题分解成哪几个子问题,分别用了什么模型。
- 求解方法:模型是怎么求的,精确解还是启发式算法,参数怎么设置。
- 核心结论:最优投放多少台、覆盖率多少、利润多少,要和正文的结果一致。
摘要不要超过一页,但必须包含 3 个以上的关键数值。比如“本文通过遗传算法与排队论相结合,在总预算 500 万元的约束下,得到最优投放点位 43 个、配置充电宝 2860 台,高峰期服务率 93.2%,年化利润 412 万元”。这样的摘要信息量充沛,评委一眼就能抓住重点。
6.2 图表设计:让结果自己说话
图表决定了论文的观感。数学建模论文最容易出现的两种错误,一种是图数量太少,另一种是图太多但全是无意义的散点。好的图表应该能独立支撑一个结论。
方案对比雷达图。用雷达图展示不同投放方案在利润、覆盖率、服务率、成本、设备利用率五个维度的表现差异,直观说明你们为什么选择最终方案。
帕累托前沿散点图。横轴是成本,纵轴是覆盖率,将遗传算法迭代过程中产生的所有非支配解画出来,并标出最终选择点。这张图是证明你们“用了多目标优化”的最好证据,评委非常吃这一套。
热力图。用地图热力图展示需求分布和投放密度的关系,最好能并排两张,左边是需求量热力图,右边是投放量热力图,两个图高度重合就说明投放方案合理。
灵敏度分析折线图。把预算、单价、需求波动系数三个参数分别 ±20% 扰动,展示利润和覆盖率的变化曲线,证明方案的稳健性。
图注要写清楚“这张图证明了什么结论”,不要光写“某结果展示”。每一张图都要有作用。
6.3 灵敏度分析:证明你的模型不是碰运气
灵敏度分析是评阅中非常看重的一环,但我见过太多队伍只是把参数改个 10% 然后再算一遍,最后写一句“结果基本稳定”。这种流于形式的灵敏度分析没有价值,需要设计更细致的扰动实验。我这里给出一个具体到可以直接照做的方案:
针对预算变化做灵敏度分析。把总预算从 300 万逐步上调到 800 万,步长取 50 万,观察最优方案中的站点数量、充电宝总数量、覆盖率和利润的变化。你大概率会看到一个典型的边际递减效应,预算从 300 增到 500 时利润快速增长,但超过 600 万后利润增长明显放缓,这个拐点就是“最优预算区间”。这段话写进论文就是非常具体的一层结论。
针对需求扰动做鲁棒性测试。在所有网格的需求预测值上分别加 ±5%、±10%、±15% 的随机扰动,每种情况重复运行模型 10 次,统计最终方案的利润均值和标准差。如果标准差相对均值控制在 5% 以内,说明方案的抗扰动能力强,这时就可以在论文里总结说“模型对需求波动具有良好鲁棒性”。
针对服务半径做对比实验。把默认服务半径 R 从 200 米改成 300 米、400 米、500 米,观察最终站点数量和覆盖率的变化。这个实验的目的是说明“为什么我们最终选取 R=300 米作为标准”,是有理有据的选择,而不是拍脑袋。
6.4 历年评分细则的启示
参考近年高教社杯和其他赛事的公开评阅要点,可以总结出评委最在意的几件事:
一是论文结构的完整性,摘要、问题分析、模型假设、模型建立、求解、验证、灵敏度分析、优缺点评价,这些部分缺一不可。
二是模型假设是否合理。比如“假设单个用户平均租借时长为 2 小时”,这个假设必须说明数据来源或给出合理依据。
三是模型的可解释性。很多队伍用神经网络做黑箱预测,但说不清楚为什么这个网络有效,这时评委就会降低评分。相比之下,线性回归、决策树等可解释性强的模型虽然精度稍低,但如果在论文里写清楚特征与目标之间的逻辑关系,分数反而更高。
四是数据处理是否扎实。如果题目给了数据,你用了多少、怎么用的、有没有做异常值处理和缺失值填补,这些过程也必须体现在论文中,不能只说“数据已预处理”。
7. 资源整合与使用建议
7.1 全套代码与数据集的组织方式
自己从零搭一套完整代码确实耗时,很多队友之间协作不好还会互相覆盖文件。我建议拿到任何资源包以后,先按下面的目录结构整理,再动手改:
/D_challenge ├── /data │ ├── raw/ # 原始题目数据 │ ├── processed/ # 清洗后的数据 │ └── generated/ # 仿真补充数据 ├── /models │ ├── demand_forecast.py # 需求预测(XGBoost/LSTM) │ ├── location_model.py # 选址模型(OR-Tools 精确解) │ ├── GA_solver.py # 遗传算法求解 │ └── SA_solver.py # 模拟退火精修 ├── /results │ ├── figures/ # 所有图表 PNG │ ├── output_solutions.csv # 最终投放方案 │ └── logs/ # 运行日志 └── /paper ├── main.tex / main.md # 论文正文 └── references.bib # 参考文献项目代码里建议至少包含三个可独立运行的文件:demand_forecast.py、location_model.py、GA_solver.py,这样每个人负责一个文件,最后用主脚本串联即可,协作效率会高很多。
7.2 多套答案版本怎么选
现在市面上的完整资源一般会提供多套“正确答案版本”,它们的差异通常来源于三个方面:模型假设不同、需求预测方法不同、算法参数不同。拿到多套版本时,不要只看最终答案数字,要重点看每一套版本的“建模假设”和“建模逻辑”。
如果某一套版本假设“需求服从泊松分布”,而另一套假设“需求服从正态分布”,这两个版本给出的最优投放量会差异很大。你需要判断哪个假设更贴近题目描述,而不是机械选择某个版本的数字。我的建议是:把多套版本作为交叉验证的参考,用其中一套做主体框架,其他版本在灵敏度分析中作为对比方案进行讨论,这样论文内容会相当丰富。
7.3 最终提交前的自查清单
作为多年带队的老手,最后提醒一下提交前要做的几件事。这份清单来自过往踩过的坑,每一条都有真实教训:
一是检查所有论文中出现的数字,你必须确保摘要、正文、结论三个部分的“站点数”“充电宝总数量”“覆盖率”“利润”四个核心数字完全一致。很多队伍在正文里写了 43 个站点,摘要里却变成 45 个,这一票之差直接被评委质疑,对论文打击极大。
二是检查所有图表是否都插入了正文,并在图注中说明结论。那些“只在附录里出现、正文完全没有提及”的图表,建议删除或者补充到正文中,否则会显得内容编排松散。
三是代码压缩包放上“README”文件,写明代码运行环境和运行顺序。哪怕你们的代码写得很精彩,评委或复现的人看不懂该先跑哪个文件,也会严重影响整体印象。
四是留出至少三个小时做降重和润色。数学建模论文很容易出现引用了别人的模板句却没有完全消化的痕迹,务必把每一段文字都改写成自己的表达方式。
资源部分用得好,能让整个比赛过程顺滑非常多。毕竟三天时间非常宝贵,把时间花在模型和论文的打磨上,才是最划算的投入。
本文还有配套的精品资源,点击获取