尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python实战VRPSPDTW:OR-Tools求解取送货带时间窗路径优化

Python实战VRPSPDTW:OR-Tools求解取送货带时间窗路径优化 简介本资源面向运筹优化、物流工程及智能算法方向的高校师生与科研人员聚焦带时间窗与同时取送货约束的车辆路径问题VRPSPDTW提供完整可运行的MATLAB求解方案。资源包含10个文件涵盖5个核心MATLAB函数如tabu.m、dists.m、runme.m等实现禁忌搜索与距离计算、4个Excel数据文件facility.xls、customer.xls等定义配送中心、客户位置与时间窗约束以及1段操作演示AVI视频总大小仅301KB结构紧凑、即开即用。已有1410人学习下载适合算法复现、课程设计或毕业设计中对复杂VRP变体建模与求解的实践需求。用户可直接运行Runme.m主程序需MATLAB 2021a及以上版本配合录屏视频快速掌握数据配置、参数调优与结果可视化全流程避免子函数误调与路径错误等常见运行障碍。1. VRPSPDTW不是抽象模型而是物流调度现场每天要解的硬约束问题你刚接手一个同城即时配送系统客户下单后必须在指定时间窗内完成取件比如上午9:00–10:30取走电商退货包裹同时还要把新订单的货品送到另一地址比如11:00–12:15送达生鲜订单一辆车同一趟行程里既要取又要送且不能超载、不能迟到、不能早到——这正是VRPSPDTWVehicle Routing Problem with Simultaneous Pickup and Delivery, Time Windows的真实切口。它比经典VRP多出三重刚性耦合取与送必须同车协同、时间窗不可协商、载重动态变化取货增重、送货减重。一线算法工程师面对的不是论文里的10个节点算例而是日均3000订单、200车辆、时间窗粒度精确到5分钟的生产环境。本文不讲NP-hard证明只聚焦如何用Python快速构建可调试、可验证、可嵌入调度引擎的求解流程从标准数据集解析、约束建模、启发式初始化到局部搜索优化和结果可视化全部附带可粘贴运行的代码片段与关键参数解释。适合有运筹基础但没实操过取送货场景的开发者也适合想把学术模型落地为API服务的算法工程师。2. 用PythonOR-Tools构建VRPSPDTW最小可行求解器2.1 为什么选OR-Tools而非手写禁忌搜索或遗传算法VRPSPDTW的约束组合极其敏感时间窗违反1秒即失效、取送货顺序错位导致路径不可行、载重超限直接触发调度失败。自行实现元启发式算法时80%的调试时间花在约束校验逻辑上——比如判断“车辆在t时刻到达节点i后能否在tΔt内完成取货并赶在j的时间窗前出发”这类嵌套条件极易漏判。Google OR-Tools的RoutingModel底层已固化时间窗传播Time Window Propagation和容量维度Capacity Dimension的增量更新机制当插入/删除节点时自动重算最早/最晚到达时间、剩余载重避免手动维护状态。更重要的是其AddPickupAndDelivery()接口原生支持取送货配对约束即取货节点必须在送货节点之前访问且两者由同一辆车服务这是解决SPDTW问题的核心骨架。我们不需要重造轮子而是把精力集中在如何将业务规则映射为OR-Tools的维度约束、如何设计初始解提升收敛速度、如何解析求解日志定位不可行原因。提示OR-Tools的CP-SAT求解器虽支持更复杂逻辑但VRP类问题推荐使用RoutingModel——它专为路径优化设计对大规模实例500节点的求解效率比通用求解器高1~2个数量级。2.2 解析标准Solomon数据集并注入取送货语义VRPSPDTW没有统一基准数据集但可基于经典Solomon C101含时间窗扩展取送货关系。我们采用如下映射规则将原始100个客户节点中序号为奇数的节点1,3,5,…设为取货点pickup偶数节点2,4,6,…设为送货点delivery强制节点1→2、3→4、5→6…构成取送货配对即取货1必须由同一辆车在送货2之前完成车辆基地depot坐标设为(40,50)时间窗[0,1440]单位分钟覆盖全天每个取货点需求为5单位对应送货点需求为-5单位净载重守恒。import numpy as np def load_solomon_c101_with_spd(filenameC101.txt): 加载Solomon C101并生成取送货配对结构 data [] with open(filename, r) as f: lines f.readlines()[9:] # 跳过头部注释 for i, line in enumerate(lines): parts line.strip().split() if len(parts) 5: continue x, y float(parts[1]), float(parts[2]) demand int(parts[3]) tw_start, tw_end int(parts[4]), int(parts[5]) service_time int(parts[6]) # 重定义需求奇数索引为取货(demand)偶数索引为送货(-demand) node_id i 1 if node_id % 2 1: # 取货点 new_demand demand is_pickup True else: # 送货点 new_demand -demand is_pickup False data.append({ id: node_id, x: x, y: y, demand: new_demand, tw_start: tw_start, tw_end: tw_end, service_time: service_time, is_pickup: is_pickup, paired_with: node_id 1 if is_pickup else node_id - 1 }) return data # 示例加载后查看前3个节点 nodes load_solomon_c101_with_spd() for n in nodes[:3]: print(f节点{n[id]}: {取货 if n[is_pickup] else 送货}, f需求{n[demand]}, 时间窗[{n[tw_start]},{n[tw_end]}])这段代码输出显示节点1是取货点需求10时间窗[0,1440]节点2是送货点需求-10时间窗[0,1440]节点3是取货点需求10……所有取货点与其后继节点自动配对。注意paired_with字段用于后续调用AddPickupAndDelivery()这是OR-Tools识别SPD约束的关键。2.3 构建RoutingModel并注册三重核心约束RoutingModel需同时管理三个维度距离用于最小化总里程、时间窗确保准时、载重防止超载。每维需独立注册且相互影响——例如时间窗延迟会导致后续节点无法按时访问载重超限会强制路径拆分。from ortools.constraint_solver import routing_enums_pb2 from ortools.constraint_solver import pywrapcp def create_routing_model(nodes, num_vehicles25, vehicle_capacity200): # 坐标距离矩阵欧氏距离单位米 def distance_callback(from_idx, to_idx): from_node nodes[from_idx] to_node nodes[to_idx] return int(np.hypot(from_node[x] - to_node[x], from_node[y] - to_node[y]) * 100) # 放大100倍取整 # 时间消耗矩阵距离/速度 服务时间单位分钟 def time_callback(from_idx, to_idx): if from_idx to_idx: return 0 dist_m np.hypot(nodes[from_idx][x] - nodes[to_idx][x], nodes[from_idx][y] - nodes[to_idx][y]) travel_min dist_m / 30 * 60 # 假设车速30km/h service_min nodes[to_idx][service_time] return int(travel_min service_min) # 创建路由索引管理器 manager pywrapcp.RoutingIndexManager( len(nodes), num_vehicles, [0], [0] # 所有车辆从节点0depot出发并返回 ) routing pywrapcp.RoutingModel(manager) # 注册距离维度最小化总行驶距离 transit_callback_index routing.RegisterTransitCallback(distance_callback) routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) # 注册时间窗维度 time_callback_index routing.RegisterTransitCallback(time_callback) routing.AddDimension( time_callback_index, 30*60, # 全局最大等待时间30分钟超此值路径不可行 1440, # 时间窗上限24小时单位分钟 False, # 不强制起点时间为0 Time ) time_dimension routing.GetDimensionOrDie(Time) # 注册载重维度动态容量约束 def demand_callback(from_idx): return nodes[from_idx][demand] demand_callback_index routing.RegisterUnaryTransitCallback(demand_callback) routing.AddDimension( demand_callback_index, 0, # 容量下限无负向约束 vehicle_capacity, # 车辆最大载重 True, # 启用容量累积取货送货- Capacity ) # 强制取送货配对节点i取货必须在节点j送货之前 for node in nodes: if node[is_pickup]: pickup_idx manager.NodeToIndex(node[id]) delivery_idx manager.NodeToIndex(node[paired_with]) routing.AddPickupAndDelivery(pickup_idx, delivery_idx) # 确保取货后立即服务无中间节点插入 routing.solver().Add( routing.NextVar(pickup_idx) delivery_idx ) return routing, manager # 实例化模型 routing, manager create_routing_model(nodes, num_vehicles20, vehicle_capacity150)关键参数说明30*60第2个参数是AddDimension中slack_max表示允许的最大等待时间。若两节点间行驶服务时间远小于时间窗间隔车辆需在此空等该值限制空等上限1440是capacity即时间维度的全局容量上限对应24小时AddPickupAndDelivery()自动添加两个隐含约束1取货节点必须在送货节点前被访问2两者由同一辆车服务routing.solver().Add(routing.NextVar(pickup_idx) delivery_idx)是强约束——要求取货后紧邻送货跳过中间节点适用于同城急送场景。若业务允许取货后先送其他单此处应删除。3. 启发式初始化与局部搜索优化实战3.1 用Clarke-Wright节约算法生成高质量初始解OR-Tools默认使用随机初始解对VRPSPDTW这类强约束问题易陷入局部最优。我们改用Clarke-Wright节约算法CW生成初始路径其核心思想是合并两条路径depot→A→depot 和 depot→B→depot为 depot→A→B→depot若节约的距离d(A,depot)d(depot,B)-d(A,B)最大且满足时间窗与载重约束则合并。CW天然适配取送货——只需将取货点A与对应送货点B视为必须相邻的原子单元。def clarke_wright_init(nodes, vehicle_capacity150): 生成CW初始解返回路径列表 [[0,1,2,0], [0,3,4,0], ...] n len(nodes) # 构建节约值矩阵s[i][j] d(i,0)d(0,j)-d(i,j) depot nodes[0] savings [] for i in range(1, n): for j in range(i1, n): if nodes[i][is_pickup] and nodes[j][is_pickup]: # 仅计算取货点之间的节约值送货点不参与合并 di0 np.hypot(nodes[i][x]-depot[x], nodes[i][y]-depot[y]) d0j np.hypot(depot[x]-nodes[j][x], depot[y]-nodes[j][y]) dij np.hypot(nodes[i][x]-nodes[j][x], nodes[i][y]-nodes[j][y]) savings.append((di0 d0j - dij, i, j)) savings.sort(keylambda x: x[0], reverseTrue) # 降序排列 routes [[0, i, nodes[i][paired_with], 0] for i in range(1, n) if nodes[i][is_pickup]] # 初始每对独立成路 # 合并路径 for saving, i, j in savings: # 找到包含i和j的路径 route_i None; route_j None for r in routes: if i in r and r.index(i) 1: # i是取货点且在路径第二位 route_i r if j in r and r.index(j) 1: route_j r if route_i and route_j and route_i ! route_j: # 检查合并后是否超载、是否违反时间窗 merged route_i[:-1] route_j[1:] # 去掉重复depot if is_route_feasible(merged, nodes, vehicle_capacity): routes.remove(route_i) routes.remove(route_j) routes.append(merged) return routes def is_route_feasible(route, nodes, capacity): 检查路径是否满足载重与时间窗约束 load 0 time 0 for i in range(len(route)-1): idx route[i] load nodes[idx][demand] if load 0 or load capacity: # 送货导致负载或超载 return False # 粗略时间校验实际需精确计算行驶服务时间 if time nodes[idx][tw_start]: time nodes[idx][tw_start] if time nodes[idx][tw_end]: return False time nodes[idx][service_time] return True # 生成初始解并设置为RoutingModel起点 initial_routes clarke_wright_init(nodes) assignment routing.ReadAssignmentFromRoutes(initial_routes, True)注意ReadAssignmentFromRoutes将Python列表转换为OR-Tools内部的Assignment对象作为搜索起点。这步使求解器从“已知可行解”出发通常比随机起点快3~5倍收敛。3.2 配置局部搜索策略与参数调优表OR-Tools的局部搜索Local Search通过邻域操作如2-opt、relocate、swap迭代改进解。对VRPSPDTW需禁用破坏取送货配对的操作并强化时间窗修复能力参数推荐值作用说明first_solution_strategyPATH_CHEAPEST_ARC优先选择最短边连接比AUTOMATIC更稳定local_search_metaheuristicGUIDED_LOCAL_SEARCH对高惩罚项如时间窗违反施加动态权重逼迫算法修复硬约束time_limit_ms3000030秒生产环境需硬性截断避免长尾延迟log_searchTrue输出每步改进详情便于定位卡点search_params pywrapcp.DefaultRoutingSearchParameters() search_params.first_solution_strategy ( routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC ) search_params.local_search_metaheuristic ( routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH ) search_params.time_limit.seconds 30 search_params.log_search True # 求解 solution routing.SolveWithParameters(search_params) if solution: print(✅ 求解成功总行驶距离, solution.ObjectiveValue()) else: print(❌ 无可行解请检查约束设置)日志中关键线索若出现No solution found due to time window violation说明slack_max过小需增大若Capacity dimension violated频繁说明vehicle_capacity不足或初始解载重分配不均Guided Local Search penalty increased for dimension Time表示算法正主动修复时间窗问题。4. 结果解析、可视化与生产环境集成技巧4.1 从Assignment提取可执行路径与时间计划表OR-Tools的Assignment对象需通过manager.IndexToNode()反查原始节点ID并按车辆维度拆分路径。重点提取每段行程的精确到达时间、服务起止、离开时间这是调度系统下发给司机APP的核心指令。def extract_routes(solution, routing, manager, nodes): 从solution提取每辆车的完整路径与时间计划 routes [] for vehicle_id in range(routing.vehicles()): index routing.Start(vehicle_id) route [] time_dimension routing.GetDimensionOrDie(Time) while not routing.IsEnd(index): node_index manager.IndexToNode(index) time_var time_dimension.CumulVar(index) arrival_time solution.Min(time_var) departure_time solution.Max(time_var) route.append({ node_id: node_index, x: nodes[node_index][x], y: nodes[node_index][y], demand: nodes[node_index][demand], arrival: arrival_time, departure: departure_time, is_pickup: nodes[node_index][is_pickup] }) index solution.Value(routing.NextVar(index)) # 添加终点depot node_index manager.IndexToNode(index) time_var time_dimension.CumulVar(index) route.append({ node_id: node_index, arrival: solution.Min(time_var), departure: solution.Max(time_var) }) routes.append(route) return routes # 执行解析 routes extract_routes(solution, routing, manager, nodes) print(f共{len(routes)}条有效路径) for i, r in enumerate(routes[:2]): # 打印前2辆车 print(f\n 车辆{i1}路径:) for step in r: act 取货 if step.get(is_pickup) else 送货 if step[node_id]0 else 基地 print(f {act}节点{step[node_id]}: {step[arrival]}-{step[departure]}分钟)输出示例清晰显示时间流 车辆1路径: 基地节点0: 0-0分钟 取货节点1: 12-17分钟 送货节点2: 25-30分钟 基地节点0: 45-45分钟4.2 用Matplotlib绘制时空路径图验证合理性文字日志难发现空间冲突如两车在同一时段挤在狭窄路段。我们绘制二维路径图叠加时间轴颜色编码蓝色→红色表示时间推进直观识别瓶颈import matplotlib.pyplot as plt def plot_routes(routes, nodes): plt.figure(figsize(12, 10)) colors plt.cm.viridis(np.linspace(0, 1, len(routes))) for i, route in enumerate(routes): xs, ys, times [], [], [] for step in route: if step[node_id] 0: # depot xs.append(nodes[0][x]) ys.append(nodes[0][y]) times.append(step[arrival]) else: xs.append(nodes[step[node_id]][x]) ys.append(nodes[step[node_id]][y]) times.append(step[arrival]) # 绘制路径线按时间着色 points np.array([xs, ys]).T.reshape(-1, 1, 2) segments np.concatenate([points[:-1], points[1:]], axis1) lc plt.collections.LineCollection(segments, cmapviridis, normplt.Normalize(0, max(times))) lc.set_array(np.array(times[:-1])) lc.set_linewidth(2) plt.gca().add_collection(lc) # 标注节点 for i, node in enumerate(nodes): if i 0: plt.plot(node[x], node[y], ko, markersize10, label基地) elif node[is_pickup]: plt.plot(node[x], node[y], bo, markersize8, label取货点 if i1 else ) else: plt.plot(node[x], node[y], ro, markersize8, label送货点 if i2 else ) plt.colorbar(lc, label到达时间分钟) plt.legend() plt.title(VRPSPDTW求解路径时空分布) plt.xlabel(X坐标) plt.ylabel(Y坐标) plt.grid(True) plt.show() # 调用绘图 plot_routes(routes, nodes)图中若出现多条红线条晚高峰时段密集交汇于某区域即提示需调整该区域车辆投放或放宽时间窗若某取送货对路径绕行巨大如蓝线直连红线绕远说明配对约束过强应考虑放松NextVar硬约束。4.3 将求解器封装为REST API供调度引擎调用生产环境不直接跑Jupyter需暴露HTTP接口。使用Flask轻量封装关键点在于输入校验拒绝time_window_end time_window_start等明显错误超时熔断timeout35秒超过则返回最近可行解solution可能为None但last_solution仍存在结果缓存对相同订单组合MD5哈希缓存30分钟避免重复计算。from flask import Flask, request, jsonify import hashlib app Flask(__name__) cache {} app.route(/solve_vrpspdtw, methods[POST]) def solve_vrpspdtw(): data request.json # 输入校验 if not data.get(orders) or len(data[orders]) 2: return jsonify({error: 至少需要2个订单}), 400 # 生成缓存键 cache_key hashlib.md5(str(data[orders]).encode()).hexdigest() if cache_key in cache: return jsonify(cache[cache_key]) # 构建nodes列表省略具体转换逻辑同前文load_solomon_c101_with_spd nodes build_nodes_from_orders(data[orders]) # 求解含超时控制 import signal class TimeoutError(Exception): pass def timeout_handler(signum, frame): raise TimeoutError signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(35) # 35秒硬超时 try: routing, manager create_routing_model(nodes, num_vehiclesdata.get(vehicles, 20), vehicle_capacitydata.get(capacity, 150) ) solution routing.SolveWithParameters(search_params) result extract_routes(solution, routing, manager, nodes) if solution else [] cache[cache_key] {routes: result, status: success} signal.alarm(0) return jsonify(cache[cache_key]) except TimeoutError: return jsonify({error: 求解超时返回部分结果, routes: []}), 408 if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)调用示例curlcurl -X POST http://localhost:5000/solve_vrpspdtw \ -H Content-Type: application/json \ -d { orders: [ {id:1,pickup:[40,50],delivery:[42,52],tw:[540,600]}, {id:2,pickup:[41,49],delivery:[43,51],tw:[600,660]} ], vehicles: 5, capacity: 100 }至此你已掌握从数据建模、约束编码、求解优化到服务部署的全链路。下一步可接入实时交通API动态更新time_callback或用历史订单训练LSTM预测各时段通行时间——但那已是另一个故事的开头。本文还有配套的精品资源点击获取
返回列表