粒子群优化算法求解车辆路径问题:Python实现与参数调优详解

📅 发布时间:2026/9/16 11:31:22
粒子群优化算法求解车辆路径问题:Python实现与参数调优详解
简介基于粒子群优化算法的车辆路径问题求解系统面向物流调度、运筹优化及算法研究等场景可解决带有容量限制的车辆路径问题CVRP通过粒子群算法自动规划车辆行驶路线实现运输成本最小化。压缩包共47个文件以Python源代码、实验图表和测试数据为主其中包含3个Python脚本、40张可视化结果图、2份数据集文件以及说明文档和缓存文件整体仅2.46MB。目前已有70人学习下载。资源内不仅提供PSO核心求解代码还额外包含ACO蚁群算法实现便于对比不同启发式算法的效果自带多组标准测试数据如E-n22-k4、E-n101-k14读者可直接运行复现优化路径并通过可视化图表直观观察算法收敛与路径规划结果适合作为毕业论文、竞赛或课程设计的参考实现。1. 拿到这个压缩包先搞清楚这一套系统在解决什么问题一个标题里同时出现“粒子群优化算法”和“车辆路径问题”的源码 zip解压后通常就是三样东西一个存放客户坐标与需求量的算例文件、一个几百行的求解主程序、一个画路线图的脚本。车辆路径问题VRP解决的是“配送中心在车辆容量和可用车数限制下怎么给客户排序让总里程最低”粒子群优化算法PSO则负责在这个庞大的排序空间里找近似最优解。程序喂进去的是一张坐标表吐出来的是几条闭环路线加一张收敛曲线。适合两类人做课程设计的同学以及不想上 OR-Tools 这类重型依赖、想看清算法每一步在干什么的工程师。它讲不了高深理论但足以把“配送排线”从拍脑袋变成可复现的实验。2. 粒子群优化算法与车辆路径问题的适配逻辑2.1 从连续位置到访问顺序SPV 编码是最常见的解码方式标准粒子群优化的粒子位置是一组连续实数而 VRP 的解是离散的客户访问顺序两者之间必须有一层“解码”。最常见的做法是 SPVSmallest Position Value最小位置值规则给每个客户分配一个实数值按值从小到大排序排出来的顺序就是访问顺序。因为排序天然是离散的粒子更新时又完全不用改连续域的速度公式这也是这类源码包里默认采用它的原因。import numpy as np # 假设 5 个客户粒子位置是 5 个实数 position np.array([0.88, -0.21, 0.56, 1.12, 0.03]) order np.argsort(position) print(order) # [1 4 2 0 3]这里order的含义是“先访问客户 1再访问客户 4再 2再 0最后 3”客户编号从 0 开始。argsort返回的是索引数组不改变粒子本身所以一次迭代里粒子位置怎么变都不影响下一轮的排序规则。需要注意排序会丢失部分邻域信息——两个实数位置相近的粒子排序结果却可能差很远这是粒子群解 VRP 时解空间邻域结构被破坏的根源后面调参数时会体现在多样性不足上。2.2 速度更新的四个控制参数pbest、gbest 之间的折中粒子群的核心是两个记忆每个粒子自己历史上最好的位置pbest和整个种群最好的位置gbest。速度更新公式在源码包里几乎都是同一副面孔r1 np.random.rand(n_particles, n_dim) r2 np.random.rand(n_particles, n_dim) vel (w * vel c1 * r1 * (pbest - position) c2 * r2 * (gbest - position)) position position vel第一项是惯性项让粒子维持原有飞行方向第二项往自己最好的位置拉第三项往种群最优拉。下面这张表是这类项目里最常改的四组量也是“跑一次没收敛”时第一个要怀疑的地方参数符号常见范围影响惯性权重w0.4 ~ 0.9越大越偏向全局搜索越小越偏向局部精修个体学习因子c11.5 ~ 2.5偏大时粒子各自为战收敛慢社会学习因子c21.5 ~ 2.5偏大时快速压向种群最优但也容易早熟速度上限v_max位置范围的 15% ~ 30%限制粒子单步位移量防飞出可行域我在调试这类源码时一般先把c1 c2 2.0固定住只动w和v_max。原因是两个学习因子作用相反同时调很难判断是哪一边出了问题而w从 0.9 降到 0.4 的线性递减基本能满足“先大范围找、再小范围挖”的阶段需求。v_max设成粒子位置范围这里位置是 0 到 1 之间的随机值范围就是 1的 20% 左右即 0.2多数小规模算例都不会飞散。2.3 粒子位置不能直接当路径用连续空间与离散空间的三个落差第一长度不定。车辆数没确定之前每条路径上装几个客户是未知的直接把粒子设计成“每辆车一段子路径”的二维数组粒子的长度就得跟着变pbest和gbest在按分量比较时会发生错位。第二交换陷阱。VRP 的解是排列排列上的小改动比如交换两个客户在连续位置空间里可能只是微小的数值变化反映在排序里却可能彻底重排导致粒子明明在靠近最优解跳一步又跳远了。第三约束暴露晚。容量约束、时间窗这类硬约束在粒子位置更新阶段完全看不出征兆只能放到解码阶段去判断“这段路径装得下吗”所以几乎所有实现都把约束塞进适应度函数里。正因为这三个落差源码包里常见的套路是粒子只管输出一个访问顺序后面单独用“容量分割”逻辑把它切成多条子路径并让适应度函数承担超载惩罚。这也是接下来建模要处理的部分。3. 车辆路径问题的建模与一个可以直接喂给求解器的算例3.1 带容量约束的 CVRP四条硬约束决定解是否合法带容量约束的车辆路径问题英文缩写 CVRP是这类源码包最常实现的版本。四条硬约束分别是每个客户恰好被一辆车服务一次每辆车从配送中心出发结束回到配送中心单车装载量不能超过容量 Q使用的车辆数不能超过可用车辆 K。前三条很好理解第四条在实际代码里经常被放松成“罚一笔钱”而不是直接判死因为元启发式算法需要探索那些“多开了一辆车但总里程下降很多”的方案如果一超车数就淘汰解常常卡在局部最优。这里的“系统”二字在这个问题设定下其实就等价于三个模块算例读取、求解器、结果输出。源码包里的求解器通常只接受“坐标 需求量 容量”作为输入所以在动手跑之前先把数据的组织方式看清楚比什么都重要。3.2 算例文件格式仓库、客户坐标与需求量怎么组织比较常见的写法是前两行放元信息后面每行一个客户。下面是一个可以直接被代码读取的 6 客户小算例第一行分别是车辆数 3 和单车载重 100第二行是配送中心坐标第三行开始依次是客户编号、横坐标、纵坐标、需求量3, 100 50, 50 1, 17, 64, 12 2, 26, 27, 18 3, 73, 87, 22 4, 64, 13, 8 5, 37, 42, 15 6, 91, 72, 16如果源码包里的数据是 CSV 或者 Excel 导出的文本字段理解起来更麻烦一点对齐规则通常是字段例子含义depot50, 50配送中心坐标所有路线从这里出发id1 ~ n客户编号仅用于结果复盘x, y(17, 64)客户坐标用于算欧氏距离demand12该客户需要占用的车辆载重读取时直接np.loadtxt(data.txt, delimiter,, skiprows2)会把客户数据全部读进来元信息用open()读前两行单独解析。有的包里会顺手把距离矩阵一次性算好存成dist数组这是可以接受的但要注意算距离矩阵用的是欧氏距离现实中如果是驾车导航距离欧氏距离算出来的结果只能当作一个不带路网信息的上界参考不代表真实油耗。3.3 适应度函数总里程优先超载和未服务用罚函数兜底适应度函数是这类 PSO 实现的核心因为算法本身不知道什么叫“合法路径”它只知道“哪个粒子位置的适应度更小”。常见形式是cost 所有车辆行驶总里程 μ * Σ 每辆车的超载量 ε * 未服务的客户数μ的经验取值是“平均单段配送距离的 100 倍左右”。取太小粒子会发现超载一点、少走很多路更划算最后所有解都超载取太大超载方案被直接压死粒子多样性快速下降。ε对应那种“单个客户需求量大于整车容量”的场景这种客户本来就无解代码里给它一个很大的惩罚值让粒子宁可损失里程也不去触碰。罚函数的设计直接决定收敛曲线是平滑下降还是剧烈震荡这也是后面排错时最先检查的地方。4. 用可运行的 Python 代码把粒子群车辆路径求解器跑起来4.1 解码函数把粒子位置切成多条合法车辆路径粒子经过 SPV 得到访问顺序后下面这个函数负责把它“切”成多条子路径顺序扫描客户累积需求量一旦加进当前客户会超过容量 Q就把这条路封掉、开一条新车。解码阶段不改粒子本身只输出路径和总里程这也是第 2 章说的“约束只在解码阶段暴露”的落点。def decode(position, customer_xy, demands, capacity, depot_xy): order np.argsort(position) routes [] route [] load 0.0 for idx in order: if load demands[idx] capacity and route: routes.append(route) route, load [], 0.0 route.append(idx) load demands[idx] if route: routes.append(route) total 0.0 for r in routes: prev depot_xy for idx in r: total np.linalg.norm(customer_xy[idx] - prev) prev customer_xy[idx] total np.linalg.norm(depot_xy - prev) return total, routes这个实现里有一个容易被忽略的关键点and route这个判断。如果某个客户自身需求量已经超过容量route是空列表此时不能盲目封路否则会生成一条空路线空路线的往返距离是 0反而把总里程“平均”下去正确做法是把它留在当前路径里让它继续参与搜索同时靠适应度函数里的ε * 未服务客户数惩罚项去阻止这类解胜出。4.2 主循环代码初始化、更新、收敛记录一次到位主体循环的骨架在源码包里大差不差差异主要在记录方式上。下面这段把初始化、速度更新、适应度评估、pbest/gbest更新和收敛记录都放在一个函数里便于直接改写def solve_vrp(customer_xy, demands, capacity, depot_xy, n_particles30, max_iter200, w_start0.9, w_end0.4, c12.0, c22.0, v_max0.2, seed42): np.random.seed(seed) n len(demands) position np.random.uniform(0, 1, (n_particles, n)) vel np.zeros_like(position) pbest position.copy() pbest_cost np.full(n_particles, np.inf) gbest_cost np.inf gbest_routes [] history [] for t in range(max_iter): w w_start - (w_start - w_end) * t / max_iter for i in range(n_particles): cost, routes decode(position[i], customer_xy, demands, capacity, depot_xy) if cost pbest_cost[i]: pbest_cost[i] cost pbest[i] position[i].copy() if cost gbest_cost: gbest_cost cost gbest position[i].copy() gbest_routes routes r1 np.random.rand(n_particles, n) r2 np.random.rand(n_particles, n) vel (w * vel c1 * r1 * (pbest - position) c2 * r2 * (gbest - position)) vel np.clip(vel, -v_max, v_max) position position vel history.append(gbest_cost) return gbest_cost, gbest_routes, history逻辑说明每次迭代先用当前粒子解码算路径代价发现更优就更新pbest或gbest所有粒子评估完后再统一更新速度和位置。np.clip(vel, -v_max, v_max)是速度钳制避免某个粒子一步跨出整个搜索空间。gbest需要在循环前初始化否则第一次迭代里比较cost gbest_cost会报未定义错误——源码包最常见的跑挂位置就在这个地方。参数说明n_particles30对应 30 只“鸟”一般 30 到 80 之间够用max_iter200表示整个种群一起飞 200 代seed固定随机种子这样同一份算例每次跑出来可复现对比实验才有意义。4.3 从终端输出到路线图怎么看结果有没有问题跑完主循环后源码包通常还会画两张图一张收敛曲线history一张最优路线图。收敛曲线的形态是最直接的体检指标。import matplotlib.pyplot as plt plt.plot(history) plt.xlabel(iteration) plt.ylabel(best cost) plt.title(PSO convergence on VRP) plt.show() plt.figure() for route in gbest_routes: xs [depot_xy[0]] [customer_xy[i][0] for i in route] [depot_xy[0]] ys [depot_xy[1]] [customer_xy[i][1] for i in route] [depot_xy[1]] plt.plot(xs, ys, markero) plt.scatter(*depot_xy, cred, s80) plt.show()如果曲线在前 20 代就平坦且很高说明种群多样性耗尽优先加大v_max或减小c2如果到最后一两代还在明显下降说明迭代数不够把max_iter翻倍再跑。路线图出现两条路线交叉得很厉害不一定是错的——因为在欧氏距离下总里程是由顺序决定的交叉只代表这个解不是局部最优可以尝试在第 5 章提到的 2-opt 改进。5. 粒子群求解车辆路径的四个必调参数与三个低成本改进5.1 惯性权重线性递减先全局探索后局部收敛固定w0.7也能跑但要获得“前期粗找、后期精修”的效果最省事的做法是让w从 0.9 线性降到 0.4。也就是 4.2 代码里的这一行w w_start - (w_start - w_end) * t / max_iter前期w大粒子速度快能覆盖较广的区域后期w小粒子在小范围内细搜。判断这个参数调没调对的方法是看收敛曲线理想曲线在 30% 迭代前明显下降之后平缓贴底。如果下降发生在最后 10 代说明权重降得太快应该把w_end提到 0.5 或直接把max_iter加大。5.2 不同算例规模下的参考参数表下面的表把两套参数直接对应到小规模和中等规模算例适合作为源码包参数文件的起点。位置范围是 0 到 1所以v_max的单位是“位置单位的比例”。参数小规模≤30 客户中等规模50~80 客户种群规模 n_particles3060最大迭代 max_iter200400惯性权重区间0.9 → 0.40.9 → 0.4c1 / c22.0 / 2.01.8 / 2.2v_max0.20.3重复运行次数510一个关键经验同样的种群规模下客户数翻倍时迭代次数不要只翻倍就以为够用因为排列组合爆炸的速度远超线性增长。把中等规模那一列当作“保底”如果 10 次运行的结果最大值与最小值差距超过 20%就把“重复运行次数”加大而不是继续堆max_iter——元启发式算法的结局要靠多次独立运行取最优来保证单一长跑更容易被早熟困住。提示改参数时一次只改一个维度否则收敛曲线变了也说不清是哪个参数引起的。5.3 三个低成本改进2-opt、模拟退火接受准则、多起点重启第一个是 2-opt。对单条子路径做两两交换把交叉边消除。常见的实现是在decode之后对每条route调用一次核心是只改写局部顺序不改粒子位置因此不会破坏 PSO 的速度连续性def two_opt(route, dist_fn): improved True while improved: improved False for i in range(1, len(route) - 1): for j in range(i 1, len(route)): if j i 1: continue # 用 dist_fn 求翻转 route[i:j] 前后的距离差 delta (dist_fn(route[i-1], route[j]) dist_fn(route[i], route[j1] if j1 len(route) else -1) - dist_fn(route[i-1], route[i]) - dist_fn(route[j], route[j1] if j1 len(route) else -1)) if delta 0: route[i:j1] route[i:j1][::-1] improved True return route这里的dist_fn是传入的距离函数-1约定为配送中心的索引delta 0表示翻转后总距离变短就执行翻转。第二个是模拟退火接受准则。在粒子个体历史最好位置更新时不只看“新解是否更优”还以概率接受差解p exp(-(new_cost - cost) / T)温度T随迭代递减。这能帮粒子跳出局部最优改动量只有几行。第三个是多起点重启外层再套一层for round in range(n_rounds)每轮重新初始化粒子位置最终记录所有轮次里的gbest。三者不冲突可以叠加使用实现难度依次递增。6. 从 zip 包到能跑的实验环境解压、依赖、对比验证6.1 解压异常先查文件损坏不是急着找第三方工具下载的 zip 出现error read zip archive或者解压到一半报could not find eocd绝大多数原因是下载不完整或文件被截断而不是压缩包加密问题。先用 7-Zip 的“测试压缩文件”功能验证或者直接重新下载真正带密码的包作者通常会在下载页或标题旁标注通用解压密码。Github 上打包下载 zip 时如果文件名带中文还容易出现解压乱码解压工具里把编码设为 UTF-8 即可。6.2 依赖检查与报错定位免费的 Python 源码最常见的跑挂点这类源码包的依赖几乎只有三个numpy、matplotlib有时加一个pandas。跑之前先做最小检查python -c import numpy, matplotlib; print(numpy.__version__, matplotlib.__version__)如果报ModuleNotFoundError直接装pip install numpy matplotlib。跑挂时优先看模块导入行和解码函数所在行90% 的问题出在“数据文件路径不对”或“读入维度不一致”不是算法逻辑错误。6.3 封装求解接口用 OR-Tools 交叉验证参数质量把 4.2 的solve_vrp包成独立接口入口只接收depot_xy、customer_xy、demands、capacity返回(gbest_cost, gbest_routes, history)这样后续接 Flask 或命令行都方便。验证参数质量时用 OR-Tools 的 Routing 模型跑同一个算例得到参考最优解or_cost然后算gap (pso_cost - or_cost) / or_cost * 100%当 gap 在 10% 以内说明这套 PSO 参数对这个规模是合格的gap 超过 20% 就返回第 5 章调权重或加大轮次。最后做一个固定种子的三组对比实验记录最好值、平均值、方差写到代码注释里比一句“运行结果很好”有说服力得多。本文还有配套的精品资源点击获取