
每年数学建模国赛和华为杯的赛题一发下来总有一批队伍在拿到题目那一刻就集体沉默。原因很简单题目里的优化问题一眼就能看懂但真要动手算梯度下降用不了、枚举又组合爆炸连现成的求解器可能都跑不动。这时候进化计算与群体智能就派上用场了。无论是路径调度、参数辨识、排班分配还是神经网络结构搜索这类启发式算法几乎成了数模竞赛里的“万能钥匙”。我写这篇文章是想把这几年带竞赛、自己参赛和帮学生跑题的经验整理出来把遗传算法、粒子群、模拟退火、蚁群这几类主流算法从原理讲到落地再完整走一遍“建模—编码—调参—验证”的实战流程。适合刚接触数学建模的新手直接抄作业也适合有基础、但总在调参和结果分析上翻车的选手查缺补漏。1. 为什么赛题里的优化问题总会想到进化计算1.1 赛题里的优化问题到底长什么样数学建模比赛里的优化题其实就三类长得最多。第一类是连续变量优化比如给一堆生产参数求成本最低的组合变量是实数目标函数可能奇形怪状。第二类是组合优化典型代表是配送路径规划、任务调度、排班考试解空间是离散的排列解的数量随规模指数增长。第三类是参数辨识或拟合类问题比如给一组观测数据让你反推某个微分方程里的未知系数。这里面有个共同特征你都能写出来一个目标函数知道什么叫做“好”但就是没法一下子找到“最好”。以我曾经碰到的一道智能仓储AGV调度题为例几十个货架位置、若干台小车、还有时间窗和电量约束检索所有可行方案的计算量大到物理上不可能。这个时候进化计算这种“用时间换最优”的思路就非常合适了。1.2 传统优化方法失灵的场景很多新手第一反应是“求导加梯度下降”。但赛题里的目标函数往往根本没给你可导的机会。比如路径优化里的总距离是“到某个解为止”才成立的分段函数排班问题里一个解是一张离散表导数无从谈起更别提那些目标函数本身就是仿真程序输出结果的“黑箱函数”。还有一类更麻烦的问题是约束极强。典型的带容量约束车辆路径问题CVRP每一辆车的载重不能超、每个客户必须访问、起终点都在仓库这些条件叠在一起会让可行域变得支离破碎。传统的线性规划或整数规划模型有时候规模小还能用分支定界法硬解一旦规模到了几百上千个点运算时间根本不是比赛能承受的。所以进化计算的核心价值在于它不要求目标函数光滑、连续、可导也不需要你对解空间结构有很深的数学洞察只要你能算出任意一个候选解的“适合程度”它就能一轮一轮迭代逼近不错的结果。这种“黑箱友好”特性让它成了数模比赛中最通用的兜底方案。1.3 进化计算与群体智能的共同底层逻辑再往深一层说进化计算和群体智能能广泛适用是因为它们共享一个底层认知与其在一个点上单打独斗地搜不如让一群候选解并行搜索靠群体层面的信息交换来平衡“开发”与“探索”。进化计算偏“演化论”视角靠选择、交叉、变异迭代更新种群群体智能偏“社会性生物”视角靠个体之间的信息共享比如粒子群里的最优位置、蚁群里的信息素来引导搜索方向。这就好比一队人进山找宝藏。梯度下降是只有一个探险家沿着最陡的方向爬很容易困在小山包上。进化计算则是空投了几百个探险家大家各自汇报“我这里海拔多少”表现差的出局表现好的相互交流重新组队探到的新区域可能完全不在原本视线里。这套逻辑天然就能避开非凸函数里的局部最优陷阱也是它在数模赛题里屡试不爽的根本原因。2. 五类主流算法速览与选型逻辑2.1 遗传算法最经典的“物种进化”遗传算法GA是绝大多数队伍入门的第一个进化算法。它的流程可以用五个词概括编码、初始化、评价、遗传操作、迭代。先确定你准备怎么表示一个可行解比如用一串实数、一段二进制串或一个排列随机生成一个种群把每个个体代入目标函数计算适应度然后通过选择算子优中选优交叉算子让两个解“生小孩”变异算子偶尔随机改动一下如此往复直到满足终止条件。我个人的体会是遗传算法最适合的问题有这么几个特征解可以自然地表示成固定长度的串适应度函数能快速计算目标函数不是特别廉价计算一次能接受但也不希望算太多。在数模比赛里调度、排班、连续函数优化、模型参数标定都能用GA硬套。类比我常给学生打的比方是你有一群应聘者每轮按照简历筛掉一半剩下的两两组队“融合简历”再随机往简历里改几个字下一轮继续筛几代之后留下来的就是比较优秀的方案。GA的核心参数不算多但每一个又都很关键。种群规模、交叉概率、变异概率、迭代代数、精英保留数量这些参数我后面会专门用一张表展开说。现在只要记住一个原则变异概率别调太高否则算法就退化成随机搜索了。2.2 粒子群算法鸟群找食物的智慧粒子群优化PSO的思路非常“动物世界”。想象一群鸟在一片完全陌生的区域里找食物每只鸟不知道食物在哪但它知道两件事自己飞过的地方里哪里食物最多以及整个鸟群里谁发现过最肥美的食物。于是每一只鸟一边朝着自己历史最佳位置飞一边又朝着全局最佳位置飞两股力量叠加出一个飞行方向不断调整速度最终群体就慢慢聚拢到食物附近。粒子群实现起来比遗传算法还简洁。每个粒子有位置和速度两个向量每一轮迭代更新速度时用三个分量叠加惯性分量保持原有方向、个体认知分量朝自己历史最优飞、社会分量朝群体历史最优飞。三个权重分别对应w、c1、c2。我一旦确定了这些系数粒子群的代码量通常不会超过八十行而且不需要编码、解码那一套适合解空间是连续实数向量的优化题。粒子群在比赛里最适合的题就是用无人机或车辆做连续轨迹参数优化、神经网络权重训练、回归模型的参数拟合。不过它也有短板如果解空间非常崎岖粒子群特别容易“早熟”也就是群体过早汇集到一个局部最优解旁边再也没法挣脱。这时候就要配合扰动机制或者局部搜索来补救。2.3 模拟退火冶金冷却带来的启发模拟退火SA不算群体算法它只有一个解但我还是把它放进这篇文章里因为它太常被用作进化算法的“精修模块”。它源于冶金学里的退火过程金属加热到高温后缓慢冷却原子才能排成能量最低的晶格结构如果冷却太快就会内应力过大、形成缺陷。算法里的“温度”就是搜索步长的控制器。高温的时候算法敢接受很差的解从而跳出局部最优温度逐渐下降接受差解的概率越来越低到了低温阶段它基本只做爬山式搜索稳稳落到一个局部最优附近。接受差解的概率用Metropolis准则如果新解比当前解差以exp(-ΔE/T)的概率接受它这里ΔE是目标函数变化量T是当前温度。数模比赛里模拟退火很少单独扛大旗我更愿意把它用在遗传算法或粒子群跑得差不多之后再对当前最优解做几百轮精细打磨。尤其是在路径规划里配合2-opt操作对配送路线做局部重排效果立竿见影。初学者最容易犯的错是降温太快恨不得20轮就把温度降到0结果退化成纯爬山搜索该跳的坑一个没跳过去。2.4 蚁群算法路径规划专用选手蚁群算法ACO的设计灵感来自蚂蚁觅食。蚂蚁在行进路上释放信息素信息素越多后来的蚂蚁越愿意走那条路而走的人多了又会留下更多信息素这就形成正反馈同时信息素会随挥发而衰减所以一条绕远的路即使偶尔被走过也会因为信息素挥发而被逐渐放弃。这个机制用来解决旅行商问题TSP和车辆路径问题是天生的合适。实现蚁群时每条边都维护一个信息素浓度每一轮迭代里每只蚂蚁按概率选择下一步访问的客户选择概率是“信息素浓度”和“距离倒数”共同加权的结果。之后按蚂蚁走过的总距离更新信息素路径越短的蚂蚁贡献越多信息素。还要设定信息素挥发系数防止老路线的信息素一直霸占位置。蚁群不是万能的。一个很现实的教训是如果问题里客户点数量超过两三百个参数不调好蚁群的收敛速度会慢得让人崩溃。相比之下遗传算法组合2-opt在同等规模下更容易出活。所以我的选型建议很明确题目本身就是路径类问题、且你愿意花时间调参用蚁群要快速稳定出结果优先GA局部搜索。2.5 差分进化与算法对比选型指南差分进化DE是国内很多优秀论文里的大热门本质上可以理解成专门为连续实数优化改造的“遗传算法换代版”。它用向量做个体变异操作不再用随机位翻转而是取两三个个体做向量差再叠加到目标个体上这样变异步长能自适应解空间的尺度。实现简单、参数少、鲁棒性极强特别适合目标函数光滑但不保证凸、变量是实数的优化题。我把这几个算法的特点整理成一张表方便选题时对照。算法编码方式核心参数适合问题收敛速度上手难度遗传算法GA二进制/实数/排列种群数、交叉率、变异率调度、排班、连续优化、组合优化中等低粒子群PSO实数向量惯性权重w、c1、c2连续优化、参数拟合、轨迹规划快低模拟退火SA任意单点初始温度、降温率局部精修、组合优化慢低蚁群ACO排列/路径信息素强度、挥发系数TSP、路径规划、网络路由慢中差分进化DE实数向量缩放因子F、交叉率CR连续函数优化、参数辨识快中我自己的选型原则是先看变量类型——实数变量直接考虑PSO或DE离散排列看是不是路径问题是就ACO配SA不是就GA。再看目标函数能不能算得快——算得快才能用大种群算得慢就做小种群加局部搜索。最后看赛题时间——时间紧张就跑PSO时间宽裕再叠GA加2-opt。3. 从一道赛题看懂完整建模流程3.1 赛题场景与问题抽象讲理论讲多了容易飘我拿一道我实际带过的赛题改编版来走一遍全流程。题目背景大致是这样某仓库接到一批配送订单仓库坐标为(50, 50)共有30个客户点客户点的坐标和需求量都已知。仓库里有5辆相同载重的配送车载重上限是40单位。要求设计配送方案使得所有客户都被访问且仅访问一次每辆车的总载重不超过上限车辆从仓库出发并返回仓库目标是最小化所有车辆行驶总距离。从大段题目文字里抽数学语言第一步永远是分清楚“决策变量、目标函数、约束条件”。这道题的决策变量是“哪些客户被分到同一辆车以及车内的访问顺序”目标函数是总行驶距离最小约束条件有容量约束、访问唯一性、起终点约束。这一步千万不能省。很多队伍拿到题就急着写遗传算法代码结果跑了半天才发现连“一个解到底代表什么”都没定义清楚。我会建议每一队先在论文里画一张关系图把目标、变量、约束一层层拆开然后再动代码。3.2 目标函数与约束的数学表达接下来要把文字翻译成公式。客户集合记为N{1,2,...,30}仓库记为0客户i的坐标是(x_i,y_i)需求量是d_i。决策变量分成两层第一层是路径划分即哪几个客户在同一辆车上第二层是路径内的顺序。总距离的计算方式是把一条路径上相邻点的欧氏距离相加。假设路径是0→c1→c2→...→ck→0那么这一段距离就是 dist(0,c1) sum(dist(ci,c(i1))) dist(ck,0)其中dist是两点间的直线距离。约束条件要一条条列出来每个客户被恰好服务一次也就是所有客户都出现在且只出现在某一条路径里。任一车辆的总载重sum(d_i) ≤ 40。每条路径都从仓库出发、最终回到仓库。决策变量的取值空间是所有合法路径划分与排列的集合。实际写论文的时候更讲究用集合和指标变量把约束写漂亮。比如用x_{ij}^k表示车辆k是否从点i直接开到点j用y_i^k表示客户i是否由车辆k服务写出流量守恒约束、容量约束和连通性约束。但代码里我不会真的按这种0-1变量去建模而是直接对“客户顺序排列”做操作因为只要解码逻辑保证“载重不超、所有客户都出现”约束天然满足。3.3 编码、适应度函数与约束处理遗传算法的第一步是编码。我强烈推荐对这类路径问题使用“顺序编码”个体是一条包含30个客户编号的排列比如[3, 15, 2, 8, ..., 21]。这个排列本身不直接对应车辆路径而是先排队再按容量约束切成多段路径。解码的方法很朴素从头开始累加需求量当加入下一个客户会超载时就在当前客户后面截断把这一段作为一辆车的路径然后开启新路径再继续累加。这样做出来的每个解100%满足容量约束候选阴影里根本不会出现非法解。这就是我用“可行化解码”而不是“罚函数法”处理约束的原因——罚函数法需要你调罚因子罚重了搜索全是爬山罚轻了得到的“最优解”可能根本不能用。适应度函数很好定义总距离越小适应度越高。由于遗传算法的选择机制通常按适应度大的个体给更多选中概率所以适应度可以取为总距离的倒数或者直接用“一个大数减去总距离”。不过要注意如果函数值之间差值很小选择压力会不足我的习惯是统一做线性缩放或排序选择避免锦标赛选择被极端值干扰。3.4 核心代码实现遗传算法解CVRP下面给出一份我能直接跑通的简化版遗传算法代码用Python实现。算法结构是随机初始化种群→锦标赛选择→顺序交叉OX→交换变异→精英保留→迭代。import random import math def distance(p1, p2): return math.hypot(p1[0] - p2[0], p1[1] - p2[1]) # 坐标和需求量示例数据30个客户 仓库 warehouse (50, 50) customers [(round(random.uniform(0, 100), 1), round(random.uniform(0, 100), 1)) for _ in range(30)] demands [random.randint(1, 10) for _ in range(30)] capacity 40 def decode(seq): 将客户排列解码为满足容量约束的多条路径 routes [] current_route [] current_load 0 for c in seq: if current_load demands[c] capacity: current_route.append(c) current_load demands[c] else: routes.append(current_route) current_route [c] current_load demands[c] if current_route: routes.append(current_route) return routes def route_distance(route): if not route: return 0.0 total distance(warehouse, customers[route[0]]) for i in range(len(route) - 1): total distance(customers[route[i]], customers[route[i1]]) total distance(customers[route[-1]], warehouse) return total def fitness(seq): routes decode(seq) return 1.0 / (sum(route_distance(r) for r in routes) 1e-6) def tournament_select(pop, k3): best random.choice(pop) for _ in range(k - 1): cand random.choice(pop) if fitness(cand) fitness(best): best cand return best[:] def ox_crossover(p1, p2): 顺序交叉保留p1的一段再按p2顺序填剩下的客户编号 size len(p1) a, b sorted(random.sample(range(size), 2)) child [-1] * size child[a:b1] p1[a:b1] fill [x for x in p2 if x not in child[a:b1]] idx 0 for i in range(size): if child[i] -1: child[i] fill[idx] idx 1 return child def mutate(seq, pm0.1): if random.random() pm: i, j random.sample(range(len(seq)), 2) seq[i], seq[j] seq[j], seq[i] return seq # 主循环 POP_SIZE 200 GENERATIONS 500 ELITE 10 pop [random.sample(range(30), 30) for _ in range(POP_SIZE)] best_seq None best_fit -1 for gen in range(GENERATIONS): pop.sort(keylambda x: fitness(x), reverseTrue) if fitness(pop[0]) best_fit: best_fit fitness(pop[0]) best_seq pop[0][:] new_pop [pop[i][:] for i in range(ELITE)] while len(new_pop) POP_SIZE: p1 tournament_select(pop) p2 tournament_select(pop) child ox_crossover(p1, p2) child mutate(child, pm0.1) new_pop.append(child) pop new_pop routes decode(best_seq) print(总行驶距离:, round(sum(route_distance(r) for r in routes), 2)) for i, r in enumerate(routes): print(车辆, i1, 路径:, r, 载重:, sum(demands[c] for c in r))这段代码的核心小技巧都在细节里。锦标赛选择里我直接调用fitness函数反复算适应度实际比赛时要把适应度缓存下来避免重复计算拖慢速度。OX交叉能最大限度保留父代里的访问顺序片段对路径类问题非常友好。变异只做简单的两两交换因为对路径问题来说“插入”变异往往更好但代码会稍微复杂一些我就先用交换版本说明原理。3.5 结果分析与稳定性验证跑完上面代码你会得到一组路径和总距离。但到这里还远远没完成——评委最看重的是你有没有证据证明“这个结果不是运气”。我的做法是固定算法不变换至少10个不同的随机种子各跑一遍记录每次的总距离、收敛代数、路径数量分布。然后算这10次结果的平均值、最优值、最差值和标准差。如果标准差很大说明算法不稳定你需要检查随机初始化覆盖面是否太窄或者变异率是不是太低导致种群多样性不够。还要画收敛曲线每一代保留当前最优适应度把它画成一条随代数变化的曲线。观察它是不是在早期快速下降、后期逐渐平稳。如果曲线在中段突然跳水说明算法并没有稳定收敛可能存在种群多样性不足或者交叉率过高的问题。数模论文里这张收敛曲线图几乎是评委一眼就要看的“标配图”。最后要做一次约束校验把最优解对应的每条路径载重、车辆数、是否覆盖所有客户一条条列出来。有时候最好解的“最优”是因为解码时悄悄丢了几个客户这种bug一旦被评委盯上整篇论文都会被打上“结果不可信”的标签。4. 参数调优与常见问题排查实录4.1 参数调优一套能拿得出手的初始配置参数调优是数模现场最耗时间的环节。我先给一套经过多次验证的初始配置适用于大多数中等规模优化问题。参数经验范围我的默认值作用种群规模50~500200种群越大多样性越好但计算越慢迭代代数200~2000500太短不够收敛太长浪费计算资源交叉概率0.6~0.950.85决定种群更新中重组比例变异概率0.005~0.20.1太低易早熟太高变随机搜索精英保留数1~2010保住历史上最好的一批解不被破坏锦标赛选择规模2~53越大选择压力越大多样性下降越快调参的通用方法是控制变量法先固定其他参数只调一个画收敛曲线对比。不要一上来就做全参数网格搜索比赛时间撑不住。还有一个我在实践中总结的细节如果适应度函数数值范围很大比如总距离从100到3000优先对适应度做归一化或排序后再做选择否则适应度最高的个体会在前几代就垄断种群。4.2 早熟收敛与局部最优怎么破早熟收敛是进化算法最经典的坑。症状非常典型前10代最优值飞速下降之后连续100代纹丝不动种群里的个体逐渐长得一模一样。原因就是种群多样性丢失得太快选择压力太大、交叉算子太单调、变异概率太低共同导致的。破法我常用的有四个。第一自适应变异。当连续多代最优解没有改进时把变异概率临时提到0.2甚至0.3等最优解开始更新后再降回来。这个逻辑很像模拟退火里的温度调节。第二精英保留加种群重启。保留当前最优的若干个体然后把其余个体全部随机重新生成相当于给种群来一次大换血。第三多种群并行。开三到五个子种群每个种群各自进化定期迁移少量最优个体到其他种群。实现起来不难效果却非常明显尤其适合赛题计算量不夸张的情况。第四引入局部搜索。遗传算法擅长广域搜索局部精修能力很弱路径类问题里加一个2-opt局部搜索对当前最优解反复做“删除两条边再重连”的操作通常能再压掉5%~15%的总距离。def two_opt(route): 对单条路径做2-opt优化反转片段看能否减少距离 improved True while improved: improved False for i in range(len(route) - 1): for j in range(i 1, len(route)): new_route route[:i] route[i:j1][::-1] route[j1:] if route_distance(new_route) route_distance(route): route new_route improved True return route这段2-opt代码写起来不难但明显增加了计算量。实际使用时要把握好频率我通常只在遗传算法收敛后对最优解精修一次而不是每一代都对每个个体去做。4.3 算法对比验证让评委相信你不是在讲故事数模论文里只写“我用遗传算法求出了结果”是远远不够的。评委会问三个问题为什么用遗传算法而不是别的遗传算法在这道题上到底好在哪里这个结果有没有可能是过拟合赛题数据得到的我的应对方式是做三组对比实验。第一组用最简单的随机搜索就是随机生成大量可行解取最优作为baseline证明进化计算确实优于瞎蒙。第二组用粒子群或差分进化跑同一个问题对比最终解得的总距离和收敛代数。第三组把“遗传算法2-opt局部搜索”和“纯遗传算法”放一起比说明局部搜索带来的增益。统计口径要统一每个算法都跑同样的随机种子数量记录最优、均值、最差、标准差、平均运行时间。然后在论文里放一张对比表格再配一组收敛曲线。这样讲道理的逻辑链就完整了先证明“我选的算法有效”再证明“我选的算法比其他算法更适合这道题”最后证明“我在算法基础上做的细节优化有价值”。这里我特别提醒一句千万不能为了好看只挑选对自己算法有利的随机种子展示结果。论文数据可以加保存的原始记录作附录评委抽检时如果发现口说无凭信誉就没了。4.4 竞赛实战里的AI辅助与自查提醒最近一年我明显感觉到AI辅助在数模竞赛里已经快成标配了。很多队伍用大模型生成思路、写代码、润色论文这本身没有错——AI在写基础算法的轮廓、解释概念、翻译专业术语上非常高效能节省大量时间。但比赛规则里通常有明确的AI使用声明要求评委会查AI率一旦发现论文主体是AI直接生成的轻则扣分重则可能影响奖项甚至取消资格。我的建议是把AI定位成“高级助教”而不是“代写枪手”。具体操作上我会让AI帮我梳理赛题建模思路、把算法伪代码翻译成Python框架、检查代码里的语法错误、润色摘要语言。但核心的决策逻辑、实验设计、结果分析、结论给出一定要自己理解后亲自写进论文。在提交之前队伍内部逐段检查论文语言和逻辑确认没有大段无法解释的AI生成文本再按各赛区要求如实填写AI使用情况说明。另外一个小技巧是把和AI的对话记录按日期留存把关键提示词和生成结果一起存成日志。这不是为了应付谁而是为了在回顾方案时能说清楚“这个思路当初是怎么来的”。对自己负责也对团队负责。关于AI提示词怎么写我个人的体会是越具体越好。不要问“这道题用什么算法”要问“一个带容量约束的车辆路径问题客户数30、车数5、载重40用什么启发式算法优先尝试编码方式怎么选”这样AI给出的答案才有真正的参考价值而不是泛泛而谈的教科书内容。写在最后的一点体会这些年带队伍最大的感受是进化计算和群体智能这类算法真正难的从来不是背公式或者抄代码而是怎么把赛题里那一大段实际问题干净利落地翻译成一个可计算、可验证、可复现的搜索问题。编码方式选得好约束天然满足后面调参就是水到渠成的事编码方式没想清楚再好的算法也救不回来。另一个想分享的经验是实验记录。我见过太多队伍头一天跑出一个不错的结果第二天想复现却怎么都对不上数最后发现是某个参数被无意中改了。我的习惯是每次跑实验都会把随机种子、参数配置、耗时、最优值、收敛代数完整记在一个表格里哪怕这个结果后来没用上这些记录也会在写论文时变成一个个数据支撑。这个方法简单但关键时刻真的能救你一把。数模比赛拼的从来不是谁知道的算法多而是谁能在有限时间内把一个方法用得稳、用得透。进化计算这个东西用得好的时候它会是你压垮对手的最后一块砝码。