简介:本资源为2022年电工杯数学建模竞赛B题「5G网络环境下应急物资配送问题」的完整解题资料包,面向参加数学建模竞赛的高校学生、需要课程设计或毕设选题的进阶学习者,以及研究车辆与无人机协同配送路径优化的技术人员。资源围绕应急场景下的物资配送建模与求解展开,涵盖问题分析、模型构建与算法实现等核心环节。压缩包共18个文件,约272KB,以11个Python脚本为主体,包含蚁群算法、TSP求解及车辆路径规划等实现代码,另附3个xlsx数据表、1份PDF赛题原文与1份README说明文档,便于对照题目复现实验。目前已有209人学习下载。读者可从中获得完整的赛题求解思路、可运行的算法脚本、原始数据与模型配置,适合作为建模入门参考或进一步改进算法的基础素材。
1. 应急物资配送遇上5G:这道题到底在考什么
2022年电工杯B题把「5G网络环境下应急物资配送」摆上台面时,很多人第一反应是套一个VRP(车辆路径问题)模型就完事。真动手才发现,题目真正想考的不是单纯的路径优化,而是当通信条件从「理想」变成「有带宽、有延迟、有中断风险」时,配送决策该怎么跟着变。换句话说,5G在这里不是背景板,它是会改变约束条件的一个变量。这道题适合已经会写基础遗传算法或蚁群算法、但没处理过「通信约束耦合进优化模型」的读者。如果你正在做数学建模竞赛、应急调度仿真,或者想找一个能把通信指标和运筹优化串起来的练手题,这篇笔记会从建模思路一路讲到代码复现和参数调优,把踩过的坑摊开说。
2. 把5G约束翻译成数学语言:从场景到模型的三步拆解
2.1 先搞清楚题目里的「5G」到底约束了什么
很多队伍一上来就写目标函数,结果写到一半发现5G这个条件根本没用上。常见做法是先把题目描述里的通信相关表述逐条抽出来,映射成可量化的约束。应急物资配送场景下,5G网络能影响决策的点通常有三个:一是车辆与调度中心之间的实时通信,决定了路径能否动态调整;二是物资需求点的状态上报延迟,影响需求预测的时效性;三是多车协同时的信息共享带宽,影响冲突消解的速度。
我一般会做一个「场景-变量-约束」的映射表,把模糊的描述逼成具体的数学条件:
| 场景描述 | 可量化变量 | 约束形式 |
|---|---|---|
| 调度中心实时掌握车辆位置 | 通信延迟 t_delay | t_delay ≤ 阈值,否则路径不可动态调整 |
| 需求点状态实时上报 | 上报周期 T_report | 需求更新频率影响重调度触发条件 |
| 多车信息共享 | 带宽 B、并发连接数 N | N × 单连接带宽 ≤ B,否则排队 |
| 网络覆盖盲区 | 覆盖概率 p_cov | 进入盲区时退化为离线决策 |
这张表的价值在于:它逼你回答「5G到底让哪个环节变快了、变准了」。如果映射完发现所有约束都和4G没区别,那说明模型没抓住题目的核心。
2.2 目标函数怎么设:别只盯着总路程
基础VRP的目标函数是最小化总行驶距离或总时间。但应急场景下,这个目标不够用。题目里「应急」两个字意味着时间窗口和惩罚成本必须进目标函数。我通常设成三层:
第一层是硬约束——物资必须在时间窗内送达,超出直接判不可行。第二层是软约束——超出时间窗但仍在可接受范围内,按延迟时长乘以惩罚系数计入成本。第三层才是行驶成本。
用数学写出来大概是这样:
min Z = α × Σ(行驶距离) + β × Σ(延迟惩罚) + γ × Σ(车辆固定成本) s.t. 时间窗约束 载重约束 通信延迟约束 覆盖约束α、β、γ三个权重不是拍脑袋定的。我的经验是先把β设得足够大,让算法优先满足时间窗,跑通之后再逐步调小,观察解的变化。如果β调小后总成本反而下降但延迟上升,说明当前路网条件下时间窗和距离存在权衡,这个权衡点就是论文里值得讨论的地方。
2.3 通信约束怎么嵌进求解过程
这是整道题最容易翻车的地方。大部分VRP代码的结构是「初始化种群 → 交叉变异 → 评估适应度 → 选择」,通信约束如果只在适应度评估时加一个惩罚项,算法很容易在迭代中期卡在不可行解附近。
更稳的做法是把通信约束分成两层处理:静态约束(如覆盖盲区)在初始化阶段就排除不可行边,动态约束(如实时延迟)在适应度评估时作为惩罚项。具体来说,构建路网邻接矩阵时,先把覆盖概率低于阈值的边直接设为无穷大,这样算法根本不会选到这些边。实时延迟则用一个简化的排队模型估算:
import numpy as np def communication_delay(n_vehicles, base_delay=0.01, bandwidth=100, msg_size=1): """ 简化通信延迟模型 n_vehicles: 当前并发车辆数 base_delay: 基础传播延迟(ms) bandwidth: 总带宽(Mbps) msg_size: 单次消息大小(Mb) """ # 带宽竞争导致的排队延迟 if n_vehicles * msg_size / bandwidth < 1: queue_delay = base_delay * (1 / (1 - n_vehicles * msg_size / bandwidth)) else: queue_delay = float('inf') # 带宽饱和,通信不可用 return queue_delay # 示例:5辆车并发时的延迟 for n in range(1, 8): d = communication_delay(n) print(f"并发车辆数 {n}, 通信延迟 {d:.4f} ms")这段代码的逻辑是:当并发车辆数增加时,带宽被分摊,排队延迟非线性上升。当负载接近带宽上限时延迟趋于无穷,代表通信不可用。参数bandwidth和msg_size需要根据题目给的具体数值调整,如果没有给,就按5G典型值设——带宽100Mbps、单条调度消息1Mb左右。这个模型不追求精确,目的是让通信约束在算法里有可感知的梯度,而不是一个非0即1的开关。
3. 用改进遗传算法跑通最小可行解:代码逐段拆
3.1 染色体编码与初始化:别用自然数编码硬套
VRP的编码方式直接决定交叉变异算子好不好写。常见的有自然数编码(染色体就是客户访问顺序)和分段编码(用分隔符切分多车路径)。这道题因为有多车和时间窗,我建议用分段编码,每个基因位代表一个客户编号,用0作为车辆分隔符。
import random def init_population(n_customers, n_vehicles, pop_size): """ 初始化种群:分段编码,0为车辆分隔符 n_customers: 客户数量 n_vehicles: 车辆数量 pop_size: 种群规模 """ population = [] for _ in range(pop_size): customers = list(range(1, n_customers + 1)) random.shuffle(customers) # 随机插入 n_vehicles-1 个分隔符 for _ in range(n_vehicles - 1): pos = random.randint(1, len(customers)) customers.insert(pos, 0) population.append(customers) return population # 示例:10个客户,3辆车 pop = init_population(10, 3, 5) for chrom in pop: print(chrom)这里的关键参数是n_vehicles。如果题目没限制车辆数,可以先按客户数除以单车容量估算一个下界,再往上浮动20%作为初始值。初始化时随机插入分隔符可能导致某辆车分到0个客户,这在后续评估时要处理——要么允许空车(增加固定成本),要么在解码时跳过。
3.2 适应度函数:通信惩罚项怎么加才不「淹没」主目标
适应度函数是算法的心脏。我见过太多代码把通信惩罚项设得过大,导致算法只顾着满足通信约束,配送成本爆炸。合理的做法是分层惩罚:硬约束违反给大惩罚(比如10000),软约束违反给中等惩罚(比如100),通信延迟按实际值线性计入。
def fitness(chromosome, distance_matrix, time_windows, comm_delays): """ 计算适应度 chromosome: 分段编码的染色体 distance_matrix: 距离矩阵 time_windows: 每个客户的时间窗 [(earliest, latest), ...] comm_delays: 每条边的通信延迟 """ routes = decode(chromosome) # 解码成多车路径 total_cost = 0 penalty = 0 for route in routes: if not route: continue load = 0 current_time = 0 prev = 0 # 从配送中心出发 for customer in route: # 行驶时间 travel_time = distance_matrix[prev][customer] / 40.0 # 假设40km/h current_time += travel_time # 通信延迟惩罚 comm_penalty = comm_delays[prev][customer] * 10 total_cost += comm_penalty # 时间窗检查 earliest, latest = time_windows[customer - 1] if current_time < earliest: current_time = earliest # 等待 elif current_time > latest: penalty += (current_time - latest) * 100 # 软约束惩罚 load += 1 # 简化:每个客户需求为1 prev = customer # 返回配送中心 total_cost += distance_matrix[prev][0] return 1.0 / (total_cost + penalty + 1e-6)参数说明:comm_delays矩阵需要提前根据2.3节的延迟模型算好,作为静态输入。惩罚系数100和10是经验值,前者针对时间窗违反,后者针对通信延迟。如果跑出来发现所有解的时间窗违反都很严重,说明惩罚系数还不够大,或者初始种群质量太差。
3.3 交叉与变异:顺序交叉配分段编码的适配
分段编码不能直接用OX(顺序交叉),因为分隔符0的位置会乱。我的做法是先把染色体按0切分成多个子路径,对每个子路径内部做OX,然后再随机重组子路径顺序。
def order_crossover(parent1, parent2): """顺序交叉,适用于单条路径(不含分隔符)""" size = len(parent1) start, end = sorted(random.sample(range(size), 2)) child = [-1] * size child[start:end] = parent1[start:end] fill_pos = end % size for gene in parent2: if gene not in child: child[fill_pos] = gene fill_pos = (fill_pos + 1) % size return child def crossover(chrom1, chrom2): """分段交叉:按车辆切分后分别交叉""" routes1 = decode(chrom1) routes2 = decode(chrom2) new_routes = [] for r1, r2 in zip(routes1, routes2): if len(r1) > 1 and len(r2) > 1: child_route = order_crossover(r1, r2) else: child_route = r1 if random.random() < 0.5 else r2 new_routes.append(child_route) # 重新编码 result = [] for i, route in enumerate(new_routes): result.extend(route) if i < len(new_routes) - 1: result.append(0) return result def mutate(chromosome, mutation_rate=0.05): """变异:随机交换两个客户""" chrom = chromosome[:] for i in range(len(chrom)): if chrom[i] == 0: continue if random.random() < mutation_rate: j = random.randint(0, len(chrom) - 1) while chrom[j] == 0: j = random.randint(0, len(chrom) - 1) chrom[i], chrom[j] = chrom[j], chrom[i] return chrom交叉和变异之后必须做可行性修复:检查每辆车的载重是否超限、客户是否重复或遗漏。修复逻辑我一般写成独立函数,在每代进化后统一调用,而不是塞在交叉算子里面。
3.4 主循环与收敛判断:什么时候该停
遗传算法的主循环不复杂,但停止条件有讲究。固定迭代次数是最简单的,但容易浪费算力或过早停止。我通常用「连续N代最优解不变」作为主停止条件,同时设一个最大迭代次数兜底。
def genetic_algorithm(n_customers, n_vehicles, pop_size=100, max_gen=500, stall_limit=50): population = init_population(n_customers, n_vehicles, pop_size) best_fitness = 0 stall_count = 0 for gen in range(max_gen): # 评估 fitness_values = [fitness(ch, dist_mat, tw, comm) for ch in population] current_best = max(fitness_values) # 收敛判断 if current_best <= best_fitness * 1.001: stall_count += 1 else: stall_count = 0 best_fitness = current_best if stall_count >= stall_limit: print(f"第{gen}代收敛,最优适应度{best_fitness:.6f}") break # 选择(锦标赛) new_pop = [] for _ in range(pop_size): i, j = random.sample(range(pop_size), 2) winner = population[i] if fitness_values[i] > fitness_values[j] else population[j] new_pop.append(winner[:]) # 交叉变异 for i in range(0, pop_size - 1, 2): if random.random() < 0.8: new_pop[i], new_pop[i+1] = crossover(new_pop[i], new_pop[i+1]) new_pop[i] = mutate(new_pop[i]) new_pop[i+1] = mutate(new_pop[i+1]) population = new_pop return best_fitness, populationstall_limit设50是一个折中值。设太小容易早停,设太大浪费时间。如果问题规模超过50个客户,可以适当放宽到80。pop_size一般取客户数的2到4倍,100是常用起点。
4. 参数调优与结果验证:让解从「能跑」到「可信」
4.1 三个必调参数:种群规模、交叉率、惩罚系数
种群规模、交叉率、惩罚系数这三个参数对结果影响最大。我的调参顺序是:先固定交叉率0.8和惩罚系数经验值,调种群规模;种群稳定后再调交叉率;最后微调惩罚系数。
| 参数 | 推荐范围 | 影响 | 调整信号 |
|---|---|---|---|
| 种群规模 | 50-200 | 太小早熟,太大慢 | 最优解波动大→增大 |
| 交叉率 | 0.6-0.9 | 太高破坏优良个体 | 收敛慢→适当降低 |
| 时间窗惩罚 | 50-500 | 太高忽略距离,太低不可行 | 不可行解多→增大 |
| 通信惩罚 | 5-50 | 太高路径绕远 | 通信延迟超标→增大 |
调参时每次只动一个,跑5次取平均,别一次改三个然后看单次结果——那是玄学,不是调参。
4.2 用小规模算例验证:手算能对上的才敢信
代码跑通之后,先用3个客户、2辆车的小算例验证。手算最优路径,和算法输出对比。如果对不上,优先检查距离矩阵和时间窗的索引是否从0开始——这是最常见的翻车点。确认小算例无误后,再逐步放大到10客户、20客户,观察运行时间和解的质量是否合理。
4.3 结果可视化:路径图和时间窗甘特图
路径图用matplotlib画散点和连线,甘特图展示每辆车的到达时间与时间窗的关系。这两张图放进论文里比表格直观得多。画图代码不复杂,关键是坐标要对应好,别把客户编号和坐标索引搞混。
5. 避坑与排查:5个让代码翻车的细节
现象:算法跑完所有客户都没被访问。原因:解码函数把分隔符0也当成客户处理了,导致路径解析错误。 解决:在decode函数里显式跳过0,并且检查每条路径是否为空。
现象:通信延迟惩罚项让总成本变成负数。原因:延迟模型在带宽饱和时返回了负值或极大值,累加后溢出。 解决:在communication_delay函数里加边界判断,饱和时返回一个大的正数而不是inf,避免数值问题。
现象:时间窗约束明明满足,适应度却很低。原因:时间窗索引和客户编号错位,比如客户1对应time_windows[1]而不是[0]。 解决:统一用「客户编号减一」作为时间窗数组索引,并在代码注释里写清楚。
现象:交叉后出现重复客户或遗漏客户。原因:分段交叉时没有做去重和补全。 解决:交叉后调用修复函数,用集合差集找出遗漏客户,随机插入到载重未满的路径中。
现象:每次运行结果差异很大。原因:随机种子没固定,或者种群规模太小。 解决:调试阶段固定random.seed(42),正式跑结果时至少跑10次取最优和平均。
6. 从竞赛题到工程落地:一个可复用的验证习惯
这道题的价值不止于竞赛。把5G约束换成任何通信条件——比如弱网、间歇连接、边缘计算延迟——模型结构不用大改,只需要替换communication_delay函数和对应的约束矩阵。我后来做类似调度问题时,养成了一个习惯:先把通信条件写成独立的约束模块,再把它挂到优化主循环上。这样换场景时只动一个文件,不用重写整个算法。
验证方法上,我推荐用「双跑对比」:一组开通信约束,一组关通信约束,看最优路径差多少。如果差值为零,说明通信约束在当前参数下没起作用,要么参数设得太松,要么模型没接对。这个对比比单纯看收敛曲线有用得多。
最后一个具体技巧:把每次实验的参数、随机种子、最优适应度写进一个CSV日志。调参时翻日志比翻代码快。我吃过亏——有一次调了二十多组参数,最后发现最优解来自第三组,但已经记不清第三组的具体配置了,只能重跑。从那以后,日志成了我的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取