简介:面向物流调度、运筹优化学习者,这份VRP问题相关程序聚焦带时间窗约束的车辆路径规划(VRPTW)问题,提供完整的算法实现与实验环境。资源共88个文件,压缩包大小1.63MB,其中txt文件(如R101、C101等算例)为Solomon基准测试数据及路径输出结果,zip压缩包内为经典solomon_25/50/100数据集,htm格式文档为Benchmarking Problems说明,另有VC++6.0工程源码、exe可执行程序和一篇基于多目标遗传算法求解VRPTW的PDF论文,便于直接运行与对照分析。已有404人学习下载。内容涵盖模型定义、遗传算法实现、适应度统计和车辆路径输出,同时附有最优解参考资料,可帮助读者快速上手VRPTW求解流程,比较不同算法性能,适合用于课程设计、毕业设计或物流路径优化的入门研究。 搞车辆路径问题(VRP,Vehicle Routing Problem)的程序,说白了就是把"人脑排车"变成"程序排车"。这事我在实际项目里做过好几轮,从最初用Excel手工调度,到后来用专门求解器跑几百个点的配送路径,中间踩的坑不少。这篇文章不是教科书,而是把我自己在VRP程序开发里用到的思路、代码、参数,以及那些查文档查不到的经验,完整沉淀一遍。适合两类人看:一是刚接触VRP、想快速上手的算法工程师,二是物流/配送系统里做调度模块、正在选型或者已经决定用程序解法替代手工排线的开发者。
- VRP的问题拆解:先搞清你在解决什么
1.1 VRP不是一道题,而是一族问题
我刚接触VRP的时候,犯了个典型错误:以为VRP就是"给一批订单找最短路线"。真正把数据摆到桌面上才发现,现实里的配送约束远比教科书复杂。
教科书里的经典VRP有一个车场、若干客户点、每个客户点有固定需求,车队从车场出发,送完货再回车场,目标是总行驶距离最短。但真实项目里几乎不会这么干净,常见的变体包括:
- CVRP(Capacitated VRP):每辆车有最大载重/容积,所有路径不能超载。这是最基础的版本,也是绝大多数系统的起点。
- VRPTW(VRP with Time Windows):客户点有可服务时间段,早到了要等,晚到了要罚。生鲜配送、同城急送基本都是这种。
- VRPPD(VRP with Pickup and Delivery):既要取货又要送货,且同一客户的取送通常要求同一辆车完成。跑腿平台、快递揽派一体就是这种情况。
- MDVRP(Multi-Depot VRP):多个车场,车辆从不同车场出发。连锁门店的补货特别常见。
程序上,这些变体看起来只是"多一个约束",但实现复杂度完全不一样。单纯的CVRP用成熟的求解器加一个容量维度就能跑;VRPTW要额外维护时间窗和车辆到达时间;VRPPD则要处理成对节点的先后顺序和同车约束。所以做程序之前,第一件事是把自己要解决的问题归类——你处理的是基础版还是带时间窗的版本,这直接决定了程序架构怎么写,也决定了后面能不能用现成工具,还是必须自己推算法。
1.2 为什么VRP程序"能跑"和"好用"是两回事
很多第一次写VRP程序的人,难点不在"理解问题",而在"程序能跑但排出来的路径没法用"。
我在一个配送项目里遇到过这种情况:用求解器跑出来的路径,总里程确实比手工排线少,但调度员一看就否决了——因为路径里有回头路,有的司机路线太长,有的司机只有两单,明显不公平。这就是典型的"数学模型最优"和"实际业务满意"之间的冲突。
原因在于,VRP程序做的是多目标折中:总成本最小只是一个目标,实际还要考虑车辆负载均衡、司机工作时长均衡、客户优先级、道路限行、停车难度。这些在模型里不是天然存在的,需要你显式地建模成约束或优化目标。所以做程序之前,务必和业务方把"什么算一条好路径"定义清楚,否则你优化出来的路径在业务眼里就是废纸。
另外一个很容易忽略的点是数据质量。VRP程序跑得再好,喂进去的距离矩阵不准、需求数据有错,输出就全盘崩。我见过一个项目,客户坐标用的是旧库,导致三成订单的地址偏移了1公里以上,程序算出来的所谓"最优路径"实际根本无法执行。程序的准确性上限,取决于输入数据质量的下限,这句话在VRP场景里体现得淋漓尽致。
- 求解器选型:别一上来就啃论文写算法
2.1 精确求解和启发式,怎么选
VRP是典型的NP-hard问题,这意味着随着客户点数量增长,求解时间会指数级膨胀。很多刚入行的人第一反应是"我写个分支定界"或者"我实现一个遗传算法"——如果只是练手可以,但生产系统里我强烈建议先用成熟的求解器,而不是自己造轮子。
现在业界的常用路线分三档:
第一档,精确求解器:比如Gurobi、CPLEX。它们能把小规模问题解到最优,对于几十个点、约束简单的CVRP,跑出来的结果就是全局最优。但到上百个点,尤其加了时间窗,求解时间可能从秒级变成小时级,甚至直接算不动。这类工具适合对最优性要求极高、规模又不太大的场景,比如工厂内部物料配送。
第二档,启发式/元启发式求解器:比如Google OR-Tools、VROOM、jsprit(Java)。它们不保证全局最优,但通常能在几秒到几十秒内给出一个工程上足够好的解。对于上百个点甚至上千个点的实际问题,这类工具是主力。
第三档,专用算法框架:比如针对TSP的LKH-3、针对VRP的VRPLIB社区算法。它们性能很强,但问题在于接口不够友好,需要你花大量时间做数据适配,而且很多算法对特定问题变体有针对性,换一个场景就要重新搞。
我的建议很直接:99%的生产项目不需要自己写算法。先确认你的问题规模,再选工具;如果OR-Tools能覆盖,就优先OR-Tools,因为社区大、资料多、API相对友好,后续好维护。
2.2 为什么我推荐OR-Tools作为第一选择
Google OR-Tools在做VRP这类组合优化问题上,有非常成熟的封装。它的核心是一个名为RoutingModel的组件,内置了多种路径构造策略和局部搜索元启发式(如模拟退火、指导式局部搜索)。最关键的是,它把"定义问题"和"搜解"解耦了,你只需要把距离矩阵、需求、车辆容量、时间窗等信息喂进去,它负责在后台做各种路径调整和优化。
它的优势有几个:
- 开箱即用,支持Python、C++、Java、.NET,对脚本语言用户友好。
- 内置的局部搜索策略在中等规模问题上效果相当不错。我在一个300个点、20辆车的场景里,用10秒让程序搜索,得到的结果比之前纯贪心构造的路径好10%-15%。
- 参数可调性强,从构造策略到元启发式算法都可以显式指定,便于做实验对比。
- 不依赖商业许可证,部署环节省心。
对比一下,VROOM的执行速度快,但约束扩展比较受限;Gurobi求精确解很强,但要在Python里做复杂的模型抽象,开发门槛高。OR-Tools在"够用、好用、可调"这三者之间,平衡做得最好。
- 核心实现:手把手写一个可运行的CVRP程序
3.1 数据和问题定义
我下面的示例使用Python和OR-Tools,解决一个经典的CVRP:一个车场,10个客户点,每辆车容量为15,目标是找总行驶距离最短的路径集合。
第一步是构造数据模型。这里的关键是定义好距离矩阵、需求列表、车辆容量和车场索引。
import numpy as np from ortools.constraint_solver import routing_enums_pb2, pywrapcp def create_data_model(): """构造VRP的输入数据。""" data = {} # 节点0是车场,1~10是客户点 data["num_clients"] = 10 coords = [ (40.0, 116.0), # 车场 (40.1, 116.2), # 客户1 (40.05, 116.15), (40.12, 116.3), (39.98, 116.1), (40.08, 116.25), (40.15, 116.2), (39.95, 116.05), (40.03, 116.28), (40.1, 116.12), (40.06, 116.18), ] # 用欧氏距离生成距离矩阵(生产环境建议换成实际道路距离) coords = np.array(coords) * 111.0 # 粗略把经纬度转为公里 n = len(coords) dist_matrix = np.sqrt(((coords[:, None, :] - coords[None, :, :]) ** 2).sum(axis=-1)).round(2) data["distance_matrix"] = dist_matrix.tolist() # 每个节点的需求,车场需求为0 data["demands"] = [0, 3, 2, 5, 4, 2, 3, 4, 5, 2, 1] # 车辆数量 data["num_vehicles"] = 3 # 每辆车的容量 data["vehicle_capacities"] = [15, 15, 15] # 车场节点索引 data["depot"] = 0 return data注意我在坐标转换上做了个简化:纬度方向乘以111公里来近似换算,这在市内范围的粗略估算是可以的,但如果跨城市,这种近似误差就会很大。生产环境建议用Haversine公式,或者直接调用地图API拿实际道路距离。后面我会专门讲距离矩阵的处理。
3.2 核心代码:注册回调、加容量约束、搜解
接下来是核心部分:用OR-Tools的RoutingModel定义模型,注册距离回调和需求回调,加上容量维度,然后指定搜索策略求解。
def solve_vrp(): data = create_data_model() manager = pywrapcp.RoutingIndexManager( len(data["distance_matrix"]), data["num_vehicles"], data["depot"], ) routing = pywrapcp.RoutingModel(manager) def distance_callback(from_index, to_index): from_node = manager.IndexToNode(from_index) to_node = manager.IndexToNode(to_index) return data["distance_matrix"][from_node][to_node] transit_callback_index = routing.RegisterTransitCallback(distance_callback) # 设置弧成本为距离 routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) def demand_callback(from_index): from_node = manager.IndexToNode(from_index) return data["demands"][from_node] demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback) # 添加容量约束。0表示不允许超出容量 routing.AddDimensionWithVehicleCapacity( demand_callback_index, 0, # null capacity slack data["vehicle_capacities"], # 每辆车最大容量 True, # start cumul to zero "Capacity", ) # 配置搜索参数 search_parameters = pywrapcp.DefaultRoutingSearchParameters() search_parameters.first_solution_strategy = ( routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC ) search_parameters.local_search_metaheuristic = ( routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH ) search_parameters.time_limit.FromSeconds(10) solution = routing.SolveWithParameters(search_parameters) if solution: print(f"总行驶距离: {solution.ObjectiveValue()}") for vehicle_id in range(data["num_vehicles"]): index = routing.Start(vehicle_id) route_nodes = [] route_demand = 0 while not routing.IsEnd(index): node = manager.IndexToNode(index) route_nodes.append(node) route_demand += data["demands"][node] index = solution.Value(routing.NextVar(index)) node = manager.IndexToNode(index) route_nodes.append(node) print(f"车辆 {vehicle_id}: 路径 {route_nodes}, 总需求 {route_demand}") else: print("没有找到可行解")这段代码里有几个关键点值得展开:
RegisterTransitCallback用于注册计算"从节点A到节点B的成本"的回调,我这里直接用距离矩阵。如果有道路限行、堵车系数,可以在这层做加权,非常灵活。AddDimensionWithVehicleCapacity是OR-Tools里加约束的核心API。第一个参数是需求回调,第二个参数是"松弛量",一般设为0表示严格不超容;第三个参数是每辆车的容量列表;第四个参数表示车辆出发时累计量从0开始。first_solution_strategy和local_search_metaheuristic是决定求解质量的关键参数。前者负责生成初始解,后者负责在初始解基础上做局部改进。
3.3 参数调优:从"能跑"到"跑得好"
很多人把程序跑通就以为结束了,实际上VRP程序的调参空间非常大。
先看first_solution_strategy。我常用的几个策略:
PATH_CHEAPEST_ARC:贪心逐条扩展路径,每次选当前节点最近的未访问节点。速度快,但解质量一般,适合快速给个初始解。GLOBAL_CHEAPEST_ARC:全局考虑所有可能的连接,每次选成本最低的边插入。通常得到的初始解比PATH_CHEAPEST_ARC好一点。SAVINGS:经典的Clarke-Wright节约算法,先默认所有点单独成行,然后计算合并路径的节约值,按节约值从大到小合并。对CVRP特别有效,我经常用它做初始解。CHRISTOFIDES:只适合TSP,因为VRP的约束太多,基本不用。
再看local_search_metaheuristic。默认的AUTOMATIC会自动选,但我通常显式指定为GUIDED_LOCAL_SEARCH(指导式局部搜索),它在跳出局部最优方面的效果很稳定。如果时间充裕,也可以用SIMULATED_ANNEALING做更充分的探索;如果追求快速出结果,GREEDY_DESCENT也可以,但容易陷入局部最优。
调参的核心思路是"时间换质量":设置time_limit越久,局部搜索的迭代次数越多,解越好,但收益是边际递减的。我在实战中的经验是,对于几百个点的规模,10-20秒是一个性价比很高的区间,超过1分钟提升幅度往往很小。遇到复杂约束时,先把模型跑通,再用不同策略做多次对比实验,而不是盲目拉长时间。
- 实战中的坑与排查技巧
4.1 没有可行解的常见原因
做VRP程序最让人头疼的就是"没有找到可行解"。这个问题90%出在数据或模型约束设置上,而不是算法本身。
第一个高发原因是车辆数量或容量不足。比如总需求是50,但所有车的总容量只有40,那必然无解。很多初学者没做前置校验,直接塞给求解器,最后得到一个令人困惑的NULL结果。我的建议是,在调用求解器之前,先写一个简单的校验:总需求是否小于等于总容量、任意一个客户点的需求是否超过单车最大容量、车辆数是否至少为1。
第二个原因是距离矩阵或需求数组的维度不匹配。OR-Tools的RoutingIndexManager要求距离矩阵是N×N的方阵,需求数组长度等于节点数。如果索引错位,有时候程序不报错,但结果荒谬,比如路径出现跳变。排查方式是把距离矩阵的shape打印出来,和节点数核对。
第三个原因出现在带时间窗的VRP上:时间窗和车辆的行驶时间严格限制导致处处不可行。这种情况通常需要放宽某个约束,比如让车辆可以早到但等待,或者将时间窗的上下界放宽几分钟。
我在做生鲜配送项目时遇到过一次:车辆必须2小时内完成所有配送,但有一个客户点距离车场单程就要1.5小时,这导致无论怎么排都无解。后来和业务方确认,这个客户的窗口期是灵活的,于是把它放到第二批配送,问题迎刃而解。所以无解时,先别急着调算法,回到业务里看约束是否真的合理。
4.2 性能瓶颈和距离矩阵的预处理
当客户点数量超过500甚至1000时,程序会明显变慢。这里的瓶颈往往是距离矩阵的计算,而不是OR-Tools的搜解本身。如果距离矩阵是N×N的,500个点就是25万个距离计算,1000个点就是100万个。如果每个距离都实时调用地图API,程序肯定跑不起来。
我的做法是分两类处理:
- 如果只需要欧氏距离或直线距离,直接用向量化计算(numpy矩阵运算),很快。
- 如果需要实际道路距离,必须做离线预处理:把所有订单坐标去重,批量调用地图API获取距离矩阵,存成缓存文件。每天更新一次即可,千万别在求解时实时逐个调用API。
另外,OR-Tools在搜解时的时间消耗也和搜索参数有关。如果出现长时间卡死,可以先调大first_solution_strategy的初始解质量,比如换成SAVINGS,让算法从更好的起点开始,减少后续局部搜索的迭代负担。
4.3 常见问题速查表
为了便于排查,我把平时遇到的典型问题整理成一个速查表:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 返回无解 | 车辆总容量小于总需求 | 增加车辆数或容量,或拆分订单 |
| 返回无解 | 单个客户需求超过单车容量 | 检查需求数据,或允许拆单(Split Delivery) |
| 路径中出现孤立点 | 距离矩阵对角线不为0或索引错位 | 检查距离矩阵,确认IndexToNode转换无误 |
| 解质量差(总里程明显偏高) | 初始解策略太贪心,局部搜索时间太短 | 换用SAVINGS,延长time_limit,启用GUIDED_LOCAL_SEARCH |
| 运行时间过长 | 节点数过多,距离矩阵庞大 | 离线缓存距离矩阵;缩小求解规模,或分区域求解 |
| 结果不公平(车辆负载差异大) | 只有总成本目标,缺少均衡约束 | 增加车辆负载均衡的目标或约束,比如限制最大最小负载差 |
- 从Demo到生产系统:数据接入与可视化
5.1 数据接入和约束扩展
Demo程序跑通后,要想真正投入使用,还有一层工程化的工作。最常见的就是数据接入:订单从哪来,坐标怎么标准化,车辆信息怎么维护。我在项目里习惯把"业务数据"和"求解模型"解耦——先用SQL或API把订单、车辆、门店数据聚合成中间表,再转换成OR-Tools的data model。这样更换数据源时,只改数据层,不动求解逻辑。
约束扩展也会在这一步暴露出问题。比如业务方要求"某辆车只能跑某几个区域""司机不能连续工作超过8小时""部分客户必须由固定司机配送"。OR-Tools里这些都可以通过维度(Dimension)来实现。维度的本质是维护一条"累计量",比如累计行驶时间、累计载重,然后对累计量施加上下界约束。理解了维度机制后,很多看似复杂的约束都能用几行代码表达出来。
5.2 距离矩阵的工程化处理
距离矩阵是整个VRP程序的地基。我强烈建议生产环境用真实道路距离,尤其是做同城配送,直线距离和实际道路距离可能差30%以上。具体实现上,可以用高德、百度的路线规划API或开源的OSRM。注意批量调用时要控频,加缓存,避免触发限流。
一个容易踩的坑是:距离矩阵的对称性问题。默认情况下,很多算法假设从A到B和从B到A距离相同,但真实道路网里因为单行道、高架桥等原因并不对称。OR-Tools支持非对称距离矩阵,但如果你用欧氏距离硬算,就要意识到这是一个近似,且某些算法可能对非对称数据更容易陷入局部最优。
5.3 可视化:让调度员愿意用你的程序
最后一个容易被忽视但很重要的点是可视化。调度员不信任"黑盒输出",如果看不到路径长什么样,他们不会用。我在项目里用folium把路径渲染到地图上,不同车辆用不同颜色,每个客户点附带需求量和时间窗信息。这样调度员在地图上一眼就能看出路径是否合理,有没有明显的回头路,有没有车辆严重偏航。
可视化还能反向帮你调试程序:有时候程序输出"最优解",但地图上看起来明显绕路,这种时候多半是距离矩阵有问题或者目标函数定义不对。别只看ObjectiveValue,一定要把路径画出来看。
踩过几次坑之后,我的习惯是:所有VRP程序的开发都从最小可跑的Demo开始,先保证输入输出链路通,加上可视化,再去调参和加约束。这样每一轮改动都能快速看到效果,也不至于到头来发现程序能跑但业务根本没法用。
如果你现在正准备写自己的VRP程序,我个人的建议是三步走:第一步用OR-Tools跑通一个简单的CVRP,把数据集、回调、容量约束这些基础概念摸熟;第二步引入真实订单数据,把距离矩阵换成实际道路距离,加上时间窗;第三步再考虑自定义约束、调优和可视化。每一步的产出都能用,不会白做。等你走完这三步,再回头去看那些学术论文里的高级算法,心里就有谱了。
本文还有配套的精品资源,点击获取