粒子群优化算法求解车辆路径问题的实现与解码技巧
2026/9/16 11:19:31 网站建设 项目流程

简介:一份面向物流、运输及相关专业学生与算法开发者的完整CVRP求解源码包。系统基于粒子群优化(PSO)算法,实现带容量限制车辆路径问题的建模、求解与结果可视化,同时集成ACO对比算法,并内置E-n22-k4、E-n101-k14等多组标准测试数据,便于验证算法效果。压缩包共47个文件,以3个Python源码与40张PNG结果图为主,另含txt数据文件、README说明等,代码结构清晰,可直接运行实验。资源大小仅2.46MB,已有70人学习浏览。通过阅读源码、运行测试数据集并参考可视化输出,读者可深入理解PSO编码、粒子位置更新、容量约束处理及路径解码等关键环节,也可在此框架上扩展改进算法,快速开展车辆路径优化相关研究。

1. 粒子群优化算法与车辆路径问题在大规模搜索上的交汇点

车辆路径问题(VRP)的组合爆炸发生在“先分车、后排序”的两层决策里:40 个客户、5 辆车时,仅分配客户到车的方案数量就已经让精确算法吃力,而每一辆车内部还得继续排访问顺序。粒子群优化算法(PSO)原本在连续函数优化上以“速度—位置”迭代著称,搬到 VRP 上要解决的并不是连续值如何逼近最优坐标,而是一组连续向量如何解码成“多辆车、多条路线的完整调度”。本文以常见的“基于 PSO 的 VRP 求解系统”源码为主线,把客户编码、容量分割、速度更新、主程序装配这几层逐一拆开,既能支撑你快速把拿到手的源码迁移到自己的数据集上,也能作为后续与 LNS、ALNS 等更强启发式做对比时的基线参考。先立住一个判断:这类求解器的质量上限,一半在解码器设计,一半在参数与重启机制。

2. 车辆路径问题的离散化建模与粒子群解码方案

2.1 车辆路径问题的三个约束与可解性边界

经典 VRP 的标准输入是:一个车场(编号 0)、N 个客户(编号 1 到 N)、车辆容量 Q、车队规模 K、每个客户的需求 d_i,以及客户之间和车场到客户的距离矩阵 C。决策结果是一组路线集合,需要满足三个约束:每条路线从车场出发并回到车场;每个客户恰好被访问一次;每条路线上的客户需求累加不超过容量 Q。目标是最小化所有路线的总行驶距离。

写成数学形式更直观:

  • min Σ (depot→首客户距离 + 客户间距离 + 末客户→depot距离)
  • s.t. 每个客户在全部路线中出现且仅出现一次
  • s.t. 每条路线的 Σ d_i ≤ Q

当客户数在 30 以内时,OR-Tools 或 CPLEX 用分支切割可以稳定逼近最优解;客户数到 80 以上,精确算法会长时间卡在定界和剪枝循环里,工程上通常转向元启发式。此时需要注意边界:PSO 在 100 客户量级的搜索效率,取决于编码和解码,而不是迭代次数。把迭代从 200 加到 2000,远不如把“粒子位置 → 客户顺序 → 车辆分割”这条链路做准确一次。

2.2 粒子位置如何变成客户顺序:SPV 排序规则

粒子是一个连续向量,而 VRP 的解是多段离散序列,第一步要建立连续向量到排列的映射。最常见的是最小位置值(Smallest Position Value, SPV)规则:粒子的每一维对应一个客户,某一维的取值越小,该客户在访问序列中的优先级越高。粒子的连续取值不携带物理坐标含义,只用于比较大小。

例如 5 个客户的粒子位置如下:

客户编号粒子维度值
10.9
20.3
31.2
4-0.4
50.8

按维度值从小到大排序,得到客户访问顺序:4 → 2 → 5 → 1 → 3。后续 PSO 的速度更新仍然只对连续向量做加减,只是在每次计算适应度之前调用一次解码函数。解码代码:

import numpy as np def decode_sequence(pos: np.ndarray) -> list[int]: # pos 的第 i 维对应客户 i+1,编号从 1 开始 idx = np.argsort(pos, kind="stable") return (idx + 1).tolist()

np.argsort返回从小到大排列的索引,加 1 转成客户编号。使用kind="stable"是为了避免两个维度取值完全相等时索引顺序抖动。需要说明,这一步输出的只是客户顺序,还没有引入车辆容量信息。

2.3 容量约束的落点:贪心分割解码器

得到客户顺序后,如果直接把所有客户塞到同一辆车里,大概率超载。常见做法是在解码环节做贪心分割:按顺序遍历客户,当前车辆还能装下就继续装,装不下就关闭当前路线、开启下一辆车。

def split_by_capacity(seq: list[int], demand: np.ndarray, capacity: float) -> list[list[int]]: routes: list[list[int]] = [] cur: list[int] = [] load = 0.0 for cust in seq: need = float(demand[cust - 1]) # demand 数组是 0 基,cust 从 1 开始 if cur and load + need > capacity: routes.append(cur) # 结算当前车辆路线 cur = [cust] load = need else: cur.append(cust) load += need if cur: routes.append(cur) return routes

执行逻辑很直接:先判断已装客户加上当前客户是否超容量,超了就收尾当前路线,把当前客户作为新路线第一个客户。这种顺序分割不考虑客户交换,所以解的质量受粒子顺序影响明显;正因如此,后续调参时可以把“分割后加 2-opt 局部搜索”作为扩展方向,但第一步先保持贪心,便于比较不同参数的效果。

2.4 初始种群结构:随机洗牌与最近邻的比例

初始化决定了解的起点分布。全部用随机排列生成粒子,覆盖空间大但平均适应度差;加入部分最近邻构造(从车场出发,每步选最近且未访问客户)能让起点质量好一个数量级,但会让种群过早集中到相似区域。工程里的常见做法是 70% 到 80% 随机排列加 20% 到 30% 最近邻粒子。第一次实验时建议只用随机初始化,否则无法判断收敛曲线的下降是 PSO 迭代产生,还是最近邻起点带来的红利。

初始化连续粒子位置的代码:

def init_population(n_pop: int, n_customers: int, rng: np.random.Generator): # 返回形状为 (n_pop, n_customers) 的连续位置矩阵 return rng.normal(loc=0.0, scale=1.0, size=(n_pop, n_customers))

用标准正态分布做初始化,取值包含正负,SPV 只关心相对排序,因此这种分布能自然覆盖大量排列。需要注意,客户规模增大时,随机初始化对排列空间的覆盖率会快速下降,此时应配合较大的惯性权重推动前期探索。

3. 粒子群优化迭代核心:速度更新与适应度函数装配

3.1 速度更新公式与连续位置的实际含义

在 VRP 的连续位置编码下,速度更新仍然用标准公式。系统中每次迭代的 Python 写法:

new_velocity = (w * velocity[i] + c1 * r1 * (pbest_pos[i] - position[i]) + c2 * r2 * (gbest_pos - position[i])) velocity[i] = new_velocity position[i] = position[i] + velocity[i]

r1、r2 是 [0,1] 区间内的均匀随机数;pbest_pos[i] 是粒子 i 迄今为止找到最优适应度时保存的连续位置;gbest_pos 是整个种群目前的最优连续位置。需要特别强调:pbest 和 gbest 保存的是连续向量,不是解码后的路线列表。如果把 pbest 保存成decode_sequence的输出,下一次迭代时的“拉力”就没有了相对大小,整个收敛方向会被破坏。

速度值本身不代表路径上的变化幅度,它只改变连续位置的大小关系,进而间接改变客户排序。因此速度更新完全沿用连续优化写法,不需要额外离散化。

3.2 一组可复用的默认参数表

参数默认值作用VRP 场景建议
n_pop50粒子数量客户 100 以内取 50,更大规模取 80 到 120
max_iter500最大迭代轮数先跑 200 看收敛曲线,再决定是否翻倍
w_start / w_end0.9 / 0.4惯性权重线性衰减探索期 w 大,后期 w 小,避免跳过好区域
c12.0个体学习因子取值 1.5 到 2.5
c22.0全局学习因子比 c1 略小可以缓解早熟
v_max0.5速度夹紧上限过大时排序变化剧烈,好解结构容易被破坏

惯性权重 w 不建议固定。常见做法是让 w 随迭代次数从 0.9 线性降到 0.4,前段保证全局探索,后段让粒子围绕历史最优位置做精细搜索。c2 如果显著大于 c1,gbest 的引力过强,粒子会在前 50 轮迅速靠拢,后续再难跳出局部最优。

3.3 适应度函数不能漏掉三段距离

适应度计算决定 gbest 的走向。一条完整路线包含三段距离:车场到该路线第一个客户的距离、路线内部客户到客户的距离、最后一个客户回到车场的距离。漏掉任何一段,求解结果都会偏向不合理的“开放路线”,导致车辆明明该回场却没回场。

def fitness(position, dist_matrix, demand, capacity, max_vehicles): seq = decode_sequence(position) routes = split_by_capacity(seq, demand, capacity) total = 0.0 for route in routes: prev = 0 # 0 表示车场 for cust in route: total += dist_matrix[prev][cust] prev = cust total += dist_matrix[prev][0] if len(routes) > max_vehicles: total += 1e4 * (len(routes) - max_vehicles) return total

这段代码里客户编号从 1 开始,而距离矩阵第 0 行和第 0 列是车场,所以dist_matrix[prev][cust]的索引不需要减 1;但demand[cust - 1]必须减 1,这是最容易出错的索引偏移。车辆数超限时加上大常数惩罚,M 的取值要比“所有值加起来”的正常最大距离高一个数量级,否则惩罚失效。

3.4 早熟停滞与粒子群变体的工程处理

VRP 的粒子群优化算法容易出现早熟:粒子聚集到 gbest 附近后,速度更新越来越小,gbest 连续几十轮没有改进。原因是没有变异机制。常见补救有三种:对 gbest 设置停滞计数,超过阈值时随机重启一部分粒子;每轮迭代后让最差粒子重新初始化;对速度做下限夹紧,避免粒子长期处于“半睡着”状态。

stall += 1 if stall >= 50: k = np.random.choice(n_pop, size=max(2, n_pop // 10), replace=False) position[k] = rng.normal(size=(len(k), dim)) stall = 0

这段逻辑放在每轮迭代末尾。重启粒子的位置重新参与下一次解码,等效于给种群注入新排列。没有这种机制的 PSO 在车辆路径问题上,表现接近“一次随机搜索加局部微调”,很难作为正式对比算法使用。

4. 跑通 PSO-VRP 求解系统:源码包结构与最小启动步骤

4.1 zip 包的典型源码布局与数据流

拿到 zip 后先看目录结构。一套完整的 PSO-VRP 系统通常至少包含数据、算法核心、业务逻辑、入口和结果输出五个部分:

目录/模块职责
data/客户坐标、需求、车辆数与容量配置
core/pso.py粒子初始化、速度更新、迭代主循环
core/vrp.py数据加载、距离矩阵计算、SPV 解码与适应度
main.py命令行入口、参数装配、结果落盘
results/路线 CSV、收敛曲线图

数据流很直接:main.py 读配置和参数,交给 vrp.py 生成距离矩阵,pso.py 初始化并迭代,最后把 gbest 解码后的结果写回 results。配置通常长这样:

{ "data_file": "data/instance_50.csv", "capacity": 80.0, "max_vehicles": 8, "n_pop": 60, "max_iter": 500, "w_start": 0.9, "w_end": 0.4, "c1": 2.0, "c2": 1.8, "random_seed": 2024 }

配置文件、命令行参数和代码内默认值三者同时存在时,普遍规则是命令行参数优先于配置文件,配置文件优先于默认值。拿到源码后最先改的位置就是这三个入口。

4.2 最小启动命令与依赖环境

标准 n 维数组运算和读取 CSV 至少需要 numpy 和 pandas。最小启动流程:

cd 解压后的目录 python -m venv .venv source .venv/bin/activate pip install numpy pandas python main.py --data data/instance_50.csv --capacity 80 --vehicles 8

--capacity对应车辆容量上限,--vehicles是可用车辆数量。这两个参数直接决定解码过程中可行解空间的大小,容量设太紧会让路线数膨胀,车辆数设太松会让算法倾向于每辆只派两个客户。如果源码没有--vehicles参数,说明配置里可能对该值写了默认值,要打开 config.json 确认。

当数据文件是 xlsx 时,读取部分需要额外依赖:

pip install openpyxl

并在 vrp.py 里把读文件方式改成pandas.read_excel(data_file, engine="openpyxl")。如果运行时报ModuleNotFoundError: matplotlib,那是结果可视化模块缺依赖;可以先装 matplotlib,或者在命令行找--no-plot开关。

4.3 输出日志怎么看:路线、负载与收敛指标

一次运行结束后的终端输出通常类似:

Iter 001/500 | gbest=260.13 Iter 100/500 | gbest=218.90 | route_count=5 Iter 500/500 | gbest=206.35 Route 1: 0 -> 4 -> 9 -> 12 -> 0 load=23/80 Route 2: 0 -> 1 -> 6 -> 11 -> 3 -> 0 load=49/80 Total distance: 206.35, vehicles=4

看结果时优先关注三点:每条路线是否以车场编号 0 开头和结尾;load 是否全程不超过容量 80;车辆使用数量是否达到设定的上限。如果多辆车满载且所有路线车辆数恰好打满车队上限,说明算法正在用压缩车辆数的方式降低惩罚项,但总距离不一定最优。此时应把车辆数惩罚从 1e4 稍微调低,让搜索更重视距离本身。

5. 验证 PSO-VRP 解质量的三个收敛技巧

拿到可运行系统后,第一件事不是把客户规模加到上千,而是先在已知最优解的小实例上验证解质量。具体做法是取一个 15 客户以内的例子,用 OR-Tools 或枚举法算出精确最优解,再用同一套粒子群优化算法连续跑 10 次,取最小适应度与均值。误差在 5% 以内,说明解码、容量分割和距离计算三条链路大概率正确;如果系统性偏大,优先检查 SPV 解码是否有索引错位,而不是急着调参。这是粒子群优化算法在车辆路径问题上最容易被忽略的闭环。

第二个技巧是把 gbest 收敛曲线作为判断收敛速度的仪表盘。正常曲线呈三段式:前 50 轮快速下降,100 到 300 轮平缓波动,后半段基本走平。若到 400 轮曲线还在明显下降,不要直接加迭代次数,先检查 v_max 是否偏大,以及惯性权重 w_end 是否过高。速度夹紧过松会让粒子持续振荡,导致线路结构反复被拆散重组。

第三个技巧是固定随机种子多次复现,统计稳定性。在 pso.py 外层写一个小循环:

seed_list = [2024, 42, 7, 100, 99] fits = [run_once(seed) for seed in seed_list] mean_fit = float(np.mean(fits)) std_fit = float(np.std(fits))

当标准差超过均值的 3% 时,说明单次运行结果置信度差,需要增加种群规模,或者把初始化中最近邻粒子的比例调低。以多次运行的最优值而非单次运行结果作为最终解,也是把 PSO 作为论文基准或生产基线时最稳妥取法。

本文还有配套的精品资源,点击获取

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

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

立即咨询