车辆路径优化实战:从TSP到VRP的Python求解指南
发布时间:2026/9/30 11:58:18来源:尧图网络
最近在折腾一个配送调度的小项目白天上班跟业务方对需求晚上回家写算法满脑子都是车辆路径优化这几个字。等我真正把一条条路线在图上铺开的时候才发现车辆路径优化这件事真的是既奇妙又折磨人。今天想从我的实战视角聊聊这个领域的入门心得包括它是怎么从一个“送快递怎么走最近”的问题变成数学模型又怎么落到真正可跑的代码里。这篇文章适合物流调度、算法工程师还有那些被老板要求“优化一下路线”但又没有头绪的朋友。哪怕你只有一点点Python基础也能跟完全程。车辆路径优化最迷人的地方在于问题描述简单到一眼就能看懂但往里深挖又是一整个算法家族。从单辆车的TSP到多辆车带容量限制的CVRP再到加上时间窗、多车场、动态订单等一系列约束每个变种都对应着真实世界里的一道具体难题。我刚开始接触的时候也交了不少学费所以这篇想把背后的逻辑、常用思路、可复现代码和踩坑经验一次性说清楚帮你少走点弯路。1. 车辆路径优化到底在解决什么问题1.1 从送快递说起VRP的直观理解想象一个很常见的场景早上仓库里有30个包裹需要送到分布在城市各处的30个客户手里只有一辆货车司机问“怎么走才能把里程跑得最少”这个问题叫旅行商问题TSP核心是找一个访问顺序。但如果包裹数量变成了300个一辆车装不下仓库里有5辆货车每辆车有载重限制有些客户还要求必须在某个时间段内送到这时候就变成了车辆路径优化问题Vehicle Routing ProblemVRP。VRP和TSP的本质区别在于TSP只有一条路线而VRP要决定“分几辆车、每辆车去哪些客户点、每辆车内部按什么顺序服务”。目标通常是最小化总行驶里程、总用车数、总配送时间或者这三者的加权组合。约束条件五花八门车辆载重、客户时间窗、司机最长工作时间、车辆必须返回仓库、某些客户只能由特定车型配送等等。真实世界里的调度问题几乎都是VRP或其变体的组合。我一开始对这个问题的复杂度没有足够敬畏。随便写几个客户点觉得很简单但随着客户数量涨到几十、上百路线组合的数量会爆炸式增长。比如10个客户点车的分配方式加上每辆车的访问顺序可能的方案数量级已经非常惊人靠人工在Excel里规划路线根本不现实。这也是为什么需要一套算法来替代人工经验。1.2 VRP的数学化定义与约束拆解从数学上看一个标准的VRP可以这样描述有1个仓库depot有n个待服务的客户点每个客户点有对应的需求量有若干辆车每辆车有固定的最大载重所有车从仓库出发并最终返回仓库。目标函数常见的是最小化所有车辆的行驶总距离或者等价地最小化总运输成本。约束条件一般分几类每个客户点必须被服务一次且只能被服务一次。每辆车服务的客户需求量总和不能超过车辆载重。每条路线必须从仓库出发最后回到仓库。如果考虑时间窗还要加上车辆到达客户点的时间落在指定区间内的限制。这些约束只是最基础的部分。实际项目中还会有“司机连续驾驶不能超过4小时”“客户只能接收某个车型”“两个订单不能拆开”等等。模型一旦复杂求解难度也跟着上升。所以很多人在做车辆路径优化时第一步不是写代码而是把约束梳理清楚确认哪些是硬约束哪些可以放宽成软约束。这个问题弄反的话后面算法再漂亮也白搭。为了帮助理解我把常见的VRP变种整理成了一张表变种名称缩写在基础VRP上新增的核心约束典型应用场景带容量限制的VRPCVRP车辆载重上限普通货物配送带时间窗的VRPVRPTW客户服务时间窗生鲜、快递时效配送带取送件的VRPVRPPD同时存在取货和送货任务逆向物流、退货回收多车场VRPMDVRP多个起始仓库车辆可从不同车场出发城市多区域配送中心动态VRPDVRP订单在过程中陆续到达需实时重规划即时配送、网约车理解自己面对的是哪种变种决定了后续选择什么算法。一开始我做项目时碰到一个带时间窗的调度需求却用CVRP的思路去建模结果算出来的路线完全不满足客户时效要求业务方直接把方案退了回来。所以建议大家花时间泡在“约束定义”这一步不要急着上算法。2. 从经典算法到启发式思路我们是怎么求解的2.1 精确算法小场景的“穷举”逻辑如果问题规模足够小理论上可以用穷举把所有可能的路线列出来再挑一个最好的。但现实是n个客户点的TSP就有n!种访问顺序VRP还要叠加车辆分配方式数量级更大。即使只有20个客户点穷举也已经慢到不可接受。于是学术界搞出了精确算法比如分支定界法、割平面法、动态规划等通过剪枝策略把不可能是最优解的分支提前砍掉从而在空间里搜索最优解。精确算法确实能保证找到全局最优解但它对问题规模非常敏感。我做过一些实验用开源求解器跑30个点的CVRP几分钟到几十分钟都算不完而到了50个点以上基本只能干瞪眼。精确算法适合验证一些小型测试用例的最优解可以用来评估其他启发式算法的优化效果。但在生产环境中订单量动辄几百上千精确算法基本不是主力。如果你只是需要一个突破口可以把它当成一把尺子先用精确算法算出一个小规模实例的最优解再对比启发式算法得到的结果就很容易知道启发式算法离最优还有多远。比如我用这个方法测过一批数据发现普通的贪心加2-opt大概能落在最优解的10%~15%范围内已经是一个很能接受的参考值。2.2 启发式与元启发式现实场景的主力现实项目里我们通常不需要严格意义的最优解而是在有限时间内要一个“足够好”的解。这时候启发式算法就登场了。最经典的包括最近邻算法从仓库出发每次都选距离当前点最近的未访问客户点加进路线。节约算法Clarke-Wright Savings计算两两客户点合并到同一条路线后比分开运输能节省多少里程按节约值从大到小合并路线。插入法把客户一个个插入到现有路线中代价最小的位置。这些启发式算法速度快思路朴实能在几毫秒内给出一个可行解。但它们的缺点也很明显容易陷入局部最优。比如最近邻算法前几个点的选择可能还好后面却被一步步带偏导致整体路线交叉、绕路严重。为了跳出局部最优元启发式算法成为进阶方案。模拟退火、遗传算法、禁忌搜索、大邻域搜索LNS等都是通过某种机制允许算法暂时接受差解从而有机会跳出局部最优的陷阱。这类算法的计算复杂度明显更高需要调的参数也更多但解质量通常能提升不少。我在实践中常用的思路是分层先用节约法生成初始路线再用2-opt、交换算子做局部搜索最后如果有时间套一层模拟退火或遗传算法的框架去迭代。这样既有速度又能保证解有竞争力。这里我特别想强调不要一上来就搞高级算法先把基础版本跑通再说不然很容易陷入调参泥潭连“算法是不是正确”都说不清楚。2.3 为什么我推荐先用贪心加局部搜索很多新手朋友会问我老师是不是直接用遗传算法最好甚至有人项目还没落地先花了一周调遗传算法的交叉概率。我觉得这是误区。因为VRP工程落地的瓶颈往往不在算法理论而在数据清洗、约束建模、系统对接这些“脏活累活”。算法越复杂调试链路越长风险也越大。我个人的项目路径是先用最朴素的贪心构造初始解再做2-opt或简单交换优化。这样实现起来只要几百行代码排查问题也不费劲而且往往能比胡乱拍脑袋的人工路线节省10%~20%的里程。等这条链路完全跑通了确认输入输出都没有问题再回头引入更复杂的算法来优化质量。用这个顺序我几乎没有失败过。下面这个对比表格是我在几组模拟数据上的实测结果可以直观感受到不同算法在速度和精度上的差异算法平均计算时间50个点解质量相对最优解实现难度最近邻约1ms偏离15%~30%极低节约算法约5ms偏离10%~20%低贪心2-opt约50ms偏离5%~15%中等模拟退火约2s偏离2%~8%较高大规模邻域搜索LNS约10s偏离1%~5%高所以我的建议很明确先实现贪心加局部搜索它能用最低的复杂度解决大部分场景问题。如果业务约束非常复杂、竞争激烈到必须压榨最后5%的成本再升级到元启发式。3. 实操用Python从零实现一个VRP求解示例3.1 数据准备与距离矩阵计算我们先从最简化的场景入手仓库在坐标原点有若干个客户点每辆车有同样的最大载重目标是让总行驶距离最小。我用Python随机生成15个客户点坐标方便演示。实际项目里客户坐标通常来自地址解析或GPS采集。import numpy as np import random # 固定随机种子保证结果可复现 random.seed(42) n_customers 15 customers [(0.0, 0.0)] # 第一个是仓库 for _ in range(n_customers): x round(random.uniform(-10, 10), 2) y round(random.uniform(-10, 10), 2) customers.append((x, y)) # 生成需求假设每车最大载重为10 demands [0] [random.randint(1, 3) for _ in range(n_customers)] capacity 10 print(客户点:, customers[1:]) print(需求量:, demands[1:])得到坐标后下一步是计算两两距离。这里有一个很多人容易忽略的坑如果直接用GPS经纬度坐标不能简单套用平面欧氏距离因为地球是个球面。在简化演示时我用的是平面坐标所以用欧氏距离没问题但真实场景如果是经纬度需要换成Haversine公式。# 计算欧氏距离矩阵 n len(customers) dist_matrix np.zeros((n, n)) for i in range(n): for j in range(n): dx customers[i][0] - customers[j][0] dy customers[i][1] - customers[j][1] dist_matrix[i][j] np.sqrt(dx * dx dy * dy) # 打印距离矩阵前几行 print(dist_matrix[:3, :3])距离矩阵是整个算法的基础后续所有的路径长度计算都会反复用到它。如果这里算错后面全盘皆错。比如在真实项目中我之前用平面距离计算导致同城路线偏差很大后来换成道路距离并调用地图API才修复。3.2 基于贪心构造初始路径先实现一个简单的“顺序分簇组内最近邻”方案。因为多辆车和单辆车的区别就在于先把客户分给哪辆车然后再决定访问顺序。由于我们的载重限制是10总需求量大概在30左右预计需要3~5辆车。最简单的分簇方式就是按需求累计超过载重就开一个新组。def split_customers_by_capacity(demands, capacity): 把客户按容量约束分成多组每组是一条路径上服务的客户集合 groups [] current_group [] current_load 0 for i in range(1, len(demands)): # 0是仓库跳过 if current_load demands[i] capacity: groups.append(current_group) current_group [i] current_load demands[i] else: current_group.append(i) current_load demands[i] if current_group: groups.append(current_group) return groups分完组之后对每一组内的客户用最近邻算法确定访问顺序。最近邻的思路很直接从仓库出发找当前点最近的未访问客户点依次走完所有客户最后回仓库。def nearest_neighbor_route(start, points, dist_matrix): unvisited set(points) route [start] current start while unvisited: next_point min(unvisited, keylambda p: dist_matrix[current][p]) route.append(next_point) unvisited.remove(next_point) current next_point route.append(start) return route def route_length(route, dist_matrix): total 0 for i in range(len(route) - 1): total dist_matrix[route[i]][route[i1]] return total然后把各组连接起来得到初始路径集合。这个初始解的质量通常一般但它是后续优化的好起点。我实测过单纯靠最近邻生成的路线往往会有明显的回头路和交叉尤其当客户点分布不均匀时。所以后面紧接着就要做局部搜索。3.3 用2-opt局部搜索优化路径2-opt是一种经典局部搜索算子原理非常直观在一条路径中找到两条不相邻的边反转它们之间的子路径。这样可以把“交叉”的路线打开重连从而缩短总里程。它之所以叫2-opt是因为一次操作同时替换了2条旧边。用代码实现2-opt时要注意一个细节反转的起点和终点不能是相邻的点否则相当于原地反转没有意义。另外每次反转后要及时更新当前路线长度避免重复计算全路径。def two_opt(route, dist_matrix, max_iterations100): best_route route.copy() best_dist route_length(best_route, dist_matrix) improved True iteration 0 while improved and iteration max_iterations: improved False for i in range(1, len(route) - 2): for j in range(i 1, len(route)): if j - i 1: continue # 反转i到j的子路径 new_route route[:i] route[i:j][::-1] route[j:] new_dist route_length(new_route, dist_matrix) if new_dist best_dist: best_route new_route best_dist new_dist improved True route best_route iteration 1 return best_route, best_dist这个双层循环里i和j遍历的是路径数组的下标不能等于首尾的仓库节点。我在第一次写的时候忘记了这一点结果把仓库也反转进子路径里导致路径看起来“很顺”但实际不符合车辆必须从仓库出发并回仓库的约束。后来花了不少时间才排查出来。建议大家在实现时对照下标画一画会比盲目写代码清楚很多。把2-opt跑在每组客户上就能得到优化后的路径。我拿上面的随机数据跑了一遍初始解总距离大概是67左右用2-opt优化后能降到56节省了约16%。你可以通过调整max_iterations来平衡时间和质量如果设置到500以上结果会更稳定。3.4 结果可视化与参数调优光看数字没有感觉把路线画出来是检查代码正确性的最直接方式。用matplotlib的话只需要把每个客户点标出来连接路线上的节点即可。import matplotlib.pyplot as plt def plot_routes(customers, routes, titleVehicle Routes): plt.figure(figsize(8, 6)) # 画仓库 plt.plot(customers[0][0], customers[0][1], ks, markersize12, labelDepot) # 画客户点 for i in range(1, len(customers)): plt.plot(customers[i][0], customers[i][1], o, colorgray) plt.text(customers[i][0], customers[i][1], str(i), fontsize9) # 画路线 colors [b, g, r, c, m, y] for idx, route in enumerate(routes): xs [customers[p][0] for p in route] ys [customers[p][1] for p in route] plt.plot(xs, ys, markero, colorcolors[idx % len(colors)], linewidth2) plt.legend(locupper right) plt.title(title) plt.show() # 初始路线 init_routes [] for group in groups: init_routes.append(nearest_neighbor_route(0, group, dist_matrix)) # 优化后路线 opt_routes [] for route in init_routes: opt_routes.append(two_opt(route, dist_matrix)[0]) plot_routes(customers, init_routes, Initial Routes) plot_routes(customers, opt_routes, Optimized Routes)参数调优是算法落地中很有趣的部分。比如2-opt的迭代次数、是否对每组随机多次重启、分簇时是否采用更优秀的扫描算法等。这些参数没有银弹需要根据数据特征实验。我的经验是先看优化前后路线图是不是出现明显交叉或绕路如果还有就增加迭代次数或引入随机重启如果已经很干净再往下一步处理时间窗等约束不要盲目堆算法。4. 工程化落地中的常见问题与排错实录4.1 订单量级与算法规模不匹配我在做一个小型配送平台的时候第一批数据只有每天80个订单用上面的贪心加2-opt方案完全够用。但业务跑起来之后订单涨到了每天500多个问题立刻出现计算时间从原来的几十毫秒飙升到几秒甚至几十秒而且解的稳定性开始变差。解决思路是分治。先把客户点按地理区域聚类比如用K-Means分成若干个簇每个簇对应一个配送片区然后对每个簇内部单独跑VRP。这样整体规模被切碎复杂度大幅下降。实际效果是500个客户点被分成8个片区后总体计算时间从20多秒压到了1秒以内。这种“先降规模再求解”的思路在工程中非常实用。很多人一看到数据量大就直接上并行计算、高级算法反而忘了最基本的分区思想。4.2 约束条件写不全导致路径不可用最常见的坑是只考虑载重忽视了时间窗和司机工作时长。我有一个同事做路由优化时算出来的路线里程确实最短但其中一条路线要让司机连续开7个小时直接违反劳动法。业务方当然不会接受。解决办法是提前和业务方一起列约束清单逐条确认。把时间窗、车辆类型、司机最大连续驾驶时间等全部整理出来。如果一个约束很难硬编码可以把它转成惩罚项比如“迟到1分钟罚10块钱”加到目标函数里。这样算法会自动避开严重超时路线又不会因为约束太死而找不到解。我比较推荐使用“软约束大惩罚权重”的做法。比如时间窗冲突按迟到时间线性惩罚但设一个上限一旦超过了上限就变成硬拒绝。这种方式既能保留求解灵活性又能保证方案业务可用。4.3 距离矩阵计算偏差与坐标转换这个是精度问题。演示代码里我用的是平面坐标但在真实项目中GPS坐标是经纬度距离应该用Haversine公式计算球面距离更严格的话还要考虑海拔。直接拿经纬度当平面直角坐标去算欧氏距离在精度要求高的场景会差很多尤其纬度越高误差越离谱。一个更贴近工程的做法是调用地图服务拿到真实的道路驾驶距离而不是直线距离。因为城区的道路网络不是直线最优算法算出来的直线最短路线放到真实路网上可能因为单行道、禁左等规则反而更远。所有数据准备阶段我会建一张“客户点间道路距离矩阵”一天跑一次离线计算存到数据库里供算法反复读取。以下是Haversine公式的简单实现可以处理经纬度点之间的球面距离from math import radians, sin, cos, asin, sqrt def haversine(lon1, lat1, lon2, lat2): R 6371.0 # 地球半径单位公里 dlon radians(lon2 - lon1) dlat radians(lat2 - lat1) a sin(dlat / 2) ** 2 cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon / 2) ** 2 c 2 * asin(sqrt(a)) return R * c需要注意Haversine公式得到的是大圆距离不是实际驾驶距离。如果业务对精度要求高仍然要走地图API。4.4 线上部署与实时调度注意点算法在离线环境下算出来是一回事上线到生产环境又是另一回事。实时调度中订单是不断进来的车辆在行进过程中还会遇到堵车、退单、新单等事件。如果每次都全量重算系统根本扛不住。一个常见的模式是滚动时域优化每隔固定时间窗口比如1分钟或5分钟把当前未分配订单和车辆位置拉出来重新优化生成新的配送计划。增量求解也很重要优先保留已出发车辆的前面一段路线只对后续未服务部分进行调整。接口设计上我建议把算法封装成一个无状态服务。输入是一份订单列表和一份车辆列表输出是每辆车的路线序列。这样调用方不用关心内部算法细节只要保证数据规范即可。同时一定要设置超时上限比如单次求解最多5秒超时则返回当前最好解而不是卡死等待。5. 从“能解”到“解得好”我在实战里沉淀的几条体会5.1 不要迷信最优解刚开始接触车辆路径优化时我总想着要拿到全局最优解最好每次都能证明自己比上一版又省了0.2%。实际上真实配送场景里司机的驾驶习惯、路况变化、客户临时改时间等不确定性远比模型里那点路线优化更影响总成本。只要算法能在几秒内给出一个比人工路线好10%以上的方案已经能创造实打实的价值。有时候为了0.5%的里程节省把计算时间从1秒拉到30秒在实时调度场景里反而是亏损的。你要的是一个稳定、快、可解释的方案而不是一个偶尔亮眼但经常超时的“黑箱最优解”。5.2 模型比算法更重要我踩过最深的坑是花大量时间研究高级算法最后发现业务方真正的痛点不是路线不够短而是订单分配规则没有定义清楚。比如“客户A和客户B必须是同一位司机配送”“大件订单不能和汤汁货物同车”“周五下午的订单要在周四就预排期”。这些业务规则落到模型里对解质量的影响比任何算法优化都大。所以每次接新项目我第一件事就是问业务方三个问题哪些约束绝对不能违反哪些成本需要最小化有没有不可能实现的需求想清楚这些算法设计才有意义。5.3 先跑通再优化如果让我给刚接触车辆路径优化的人一句建议那就是先把手里的数据变成可计算的模型再用最简单的算法跑出一条基础路线。哪怕它很粗糙至少你有了一个可复现的“对照组”。接下来每次升级算法都比一比有没有变好变化是正向的还是负向的心里就有数了。很多人一上来就搭建遗传算法加并行计算的大工程结果一个bug查了三天最后甚至不知道当前解到底可不可行。真正靠谱的路线是贪心出解2-opt改良再围绕实际约束做精细化建模。等你把这条路走通再谈高级优化也不迟。我最后一次做这个项目时其实只用了最基础的贪心加2-opt但因为数据清洗和约束建模做得足够扎实最终方案反而比之前用遗传算法跑出来的还要实用。这个案例让我彻底明白车辆路径优化既是算法的竞技场更是工程和业务理解的角斗场。希望这篇文章能让你少踩几个坑更快找到属于自己的“最短路径”。
网站建设高端定制企业官网