节约里程法实战:末端配送路径优化的启发式算法解析

发布时间:2026/9/20 0:48:55
节约里程法实战:末端配送路径优化的启发式算法解析 简介这是一份城市电商物流末端配送路径优化方向的完整研究文档以京东物流广州客村站为实证对象结合节约里程法对配送线路进行建模与求解适合物流管理、电子商务及运筹优化方向的学生作为论文写作参考。文档从城市物流现状、电子商务概念、配送路线优化理论出发梳理配送规划中的时间、成本、距离、交通流量等约束因素并通过实地访谈与电话访问收集一线配送站数据剖析末端配送管理不严、线路规划不科学等问题在此基础上给出节约里程法应用后的优化配送线路以及针对性的改进建议兼顾理论意义与现实意义。资源为单份 docx 文档压缩包约 1.12MB内容包含完整中英文摘要、关键词、正文章节及研究方法整理便于直接查阅、引用或二次改编。已有155人学习下载适合需要快速理解城市末端配送优化思路、借鉴真实站点案例的研究者与物流从业者。1. 为什么末端配送路径优化绕不开节约里程法客村站每天的订单在午前和傍晚各有一个峰值配送员、三轮车和两轮车混行在城中村、老旧小区和写字楼三片完全不同的路网里。调度凭经验画出的路线经常出现两个骑手在同一条巷子里擦肩而过。这不是管理问题而是规划问题。节约里程法Clarke-Wright Savings Algorithm解决的是从单一配送中心出发、向多个客户点配送后返回的场景它用两个客户点合并后的距离减少量作为排序依据把整张路径表一条条拼出来。对于城市电商物流末端配送路径优化这套算法比精确求解器更贴近实操速度快、可解释、易叠加约束。下面就以广州客村站为背景从距离矩阵、路径合并到时间窗修正拆一套能直接落地的末端配送优化方案。适合处理坐标、订单和车辆约束的物流规划、运筹或后端开发者参考。2. 节约里程法的数学基础和VRP选型逻辑末端配送路径优化在运筹学里属于带容量约束的车辆路径问题CVRP。客村站每天的订单规模通常在几十到几百个客户点如果每个订单拆成一个客户点问题规模不大但约束很多。精确算法在客户点超过五十个后会很快进入指数级搜索调度员需要的是几分钟内能出结果、并且支持手工调整的方案。节约里程法就是在这种场景里被反复使用的基础启发式算法它不只是给出一个可行解更能在后续作为局部搜索的初始解继续逼近最优。2.1 距离节约值到底在节约什么从站点0出发分别给客户点i和j单独配送总距离是2d0i加2d0j。把i和j放到同一条路径上路线变成0-i-j-0总距离是d0i加dij加dj0。两条路线的差值就是节约里程法的核心S(i,j)等于d0i加d0j减去dij。S值越大说明两个客户点合并后节省的距离越多也就越值得放到同一条行驶路线上。这个公式默认距离对称且车辆从站点出发配送完返回站点。末端场景中站点到客户的距离短客户之间距离也短当dij接近d0i加d0j时说明两个客户方向相反或者中间有路网阻隔合并价值很低当S值大时说明两个客户基本同向合并后可以减少重复路段。把全部客户对的S值降序排列再按顺序尝试合并路径这就是1964年Clarke和Wright提出的经典算法。算法价值不在公式本身而在排序后的“合并规则”不重新规划全局只在已有路径片段上不断拼接。2.2 为什么末端场景优先选节约里程法而不是精确求解器VRP是NP-hard问题分支定界、割平面这类精确算法在二十个客户点内表现不错到六十个以上就明显吃力。末端配送的典型规模是每天每个站点上百个订单还要考虑司机执行、临时改址、异常签收路径不能算太久。节约里程法求的是构造解计算时间通常在毫秒级解的质量不会离最优解太远。更关键的是它可以作为2-opt、Or-opt等局部搜索的初始路线形成“构造启发式加局部搜索”的组合。先跑节约里程法再用2-opt平滑路径是末端优化里最稳的起点。如果直接上遗传算法或模拟退火参数多、调参成本高现场调度很难维护。节约里程法的优势是每个合并决策都能用节约值解释“这两个订单为什么放同一辆车因为能省多少公里”。这一点在业务侧很重要配送站长能看懂才能接受算法结果。2.3 客村站场景的数据假设与输入字段客村站位于广州海珠区订单密集分布在城中村、赤岗片区和新港路沿线。配送员以电动车为主单车装载重量和体积都有上限。要把路径优化量化至少需要四类输入订单编号、经纬度或平面坐标、需求重量或体积、时间窗。下面模拟一个十个客户点的输入配送站坐标取原点订单坐标用平面米为单位方便直接计算欧氏距离。客户点x(m)y(m)需求(kg)时间窗112080810:00-12:002-901401210:00-11:30320030514:00-16:004-150601510:30-13:00540-110711:00-14:006180-80914:30-16:307-60-1601113:00-15:008100160610:00-12:309-140-901014:00-15:3010250110415:00-17:00这里没有使用真实订单数据只是用坐标模拟客村站周边可能出现的分布。实际落地时把PDA订单地址做地理编码转成WGS84坐标再投影到平面就能得到同样的输入表。算法不关心坐标来源只关心距离矩阵是否可靠。3. 用Python把节约里程法在客村站数据上跑通写代码前要明确合并规则否则容易写出“看起来在跑、实际路径交叉”的版本。每辆车对应一条客户点序列序列顺序就是访问顺序默认从站点0出发最后返回站点0。合并两个客户点时只允许一条序列的末尾点连到另一条序列的开头点这样所有路径仍然保持“从站点出发、最终回站点”的结构。如果允许任意位置拼接会破坏先后关系后续也没法做时间窗检查。3.1 距离矩阵和初始路径import math # 站点为0客户点从1开始编号 coords { 0: (0.0, 0.0), 1: (120.0, 80.0), 2: (-90.0, 140.0), 3: (200.0, 30.0), 4: (-150.0, 60.0), 5: (40.0, -110.0), 6: (180.0, -80.0), 7: (-60.0, -160.0), 8: (100.0, 160.0), 9: (-140.0, -90.0), 10: (250.0, 110.0), } demands {0: 0, 1: 8, 2: 12, 3: 5, 4: 15, 5: 7, 6: 9, 7: 11, 8: 6, 9: 10, 10: 4} capacity 40 # kg客村站常见的配送车载重上限 def dist(a, b): return math.hypot(coords[a][0] - coords[b][0], coords[a][1] - coords[b][1]) def build_distance_matrix(nodes): return {i: {j: dist(i, j) for j in nodes} for i in nodes} nodes list(coords.keys()) dmat build_distance_matrix(nodes) # 初始方案每个客户点单独一条路径 routes [[i] for i in range(1, len(coords))] print(初始路线数量:, len(routes))coords和demands构成算法输入需求单位用公斤容量设为40公斤覆盖模拟数据。build_distance_matrix生成全连接距离矩阵客户点少于500时内存完全够用。初始路径把每个客户点单独开一路后续通过合并减少车辆数。注意routes只存客户序列计算总距离时才补上两侧的站点0。3.2 计算节约值并执行路径合并# 计算所有客户对的节约值并按降序排序 savings [] for i in range(1, len(coords)): for j in range(i 1, len(coords)): s dmat[0][i] dmat[0][j] - dmat[i][j] savings.append((s, i, j)) savings.sort(reverseTrue) def total_demand(route): return sum(demands[node] for node in route) def total_route_distance(route): return dmat[0][route[0]] sum(dmat[route[k]][route[k 1]] for k in range(len(route) - 1)) dmat[route[-1]][0] # 按节约值顺序尝试合并 for s, i, j in savings: route_i None route_j None for route in routes: if i in route: route_i route if j in route: route_j route if route_i is None or route_j is None or route_i is route_j: continue # 只允许一条路线的末尾点连接另一条路线的开头点 if route_i[-1] i and route_j[0] j: new_route route_i route_j elif route_j[-1] j and route_i[0] i: new_route route_j route_i else: continue if total_demand(new_route) capacity: continue routes.remove(route_i) routes.remove(route_j) routes.append(new_route) print(合并后路线:) for route in routes: print( , route, 距离, round(total_route_distance(route), 1), 载重, total_demand(route)) print(总车辆数:, len(routes), 总里程:, round(sum(total_route_distance(r) for r in routes), 1))合并逻辑里有两个关键判断。第一个if处理“i在route_i末尾、j在route_j开头”的方向合并后新路线为route_i加route_j第二个elif处理反向连接等价于把route_j放前面再接route_i。这样两条子路径可以不反转地拼成一条连续路径。容量约束在合并前检查超过40公斤就放弃这次合并继续看下一个节约值。输入对象数据类型含义coordsdict客户点和站点的平面坐标demandsdict每个客户点的需求重量capacityint/float单车最大载重dmatdict全连接距离矩阵savingslist节约值降序列表3.3 结果输出和容量约束的作用上面代码跑完能看到车辆数从10降下来。比如某个典型输出可能形成四到五条路径每条路径三到四个客户点总里程显著下降。容量约束在这里起的作用是防止把距离近但需求大的客户点强行拼到一起。末端配送中常常出现“两个订单离得很近但加起来超重”的情况节约值排序会优先尝试合并它们如果没有容量检查最后算出来的路线物理上不可行。在货车或电动车的实际载重中容量不是单一数值而是重量和体积双约束。代码里只用重量有一个潜在问题占体积大但重量轻的生鲜、冷冻订单会被低估。后续应改成“重量不超过容量且体积不超过车厢容积”的双重判断。客村站的站点数据里如果某个订单本身超过容量在初始路径阶段就会让每辆单独配送的车超载所以建议在算法入口处先做一次单点校验。4. 时间窗、车容量和实际道路约束的修正基本节约里程法不处理时间窗但城市电商物流末端配送绕不开“午前达”“预约达”这类时效要求。只有把时间窗约束加进合并可行性判断优化结果才真正能排班。时间窗需要在两个地方生效一是在合并路径时拒绝“到达时间晚于截止时间”的新路线二是在最终输出时给出每条路径的预计到达时间方便站长排班。4.1 时间窗可行性判断给客户点设定最早可配送时间和最晚可配送时间用分钟表示。7点30分从站点出发电动车在客村片区的平均速度按20公里/小时估计服务时间按2分钟一个点估算。检查路径时每到一个客户点累加行驶时间早于时间窗起点就等待晚于时间窗终点就判定不可行。tw_start {1: 600, 2: 600, 3: 840, 4: 630, 5: 660, 6: 870, 7: 780, 8: 600, 9: 840, 10: 900} tw_end {1: 720, 2: 690, 3: 960, 4: 780, 5: 840, 6: 990, 7: 900, 8: 750, 9: 930, 10: 1020} def check_time_window(route, dmat, service_time2.0, speed_kmh20.0): # 7:30出发换算成分钟 clock 7.5 * 60 for k, node in enumerate(route): if k 0: travel dmat[0][node] / 1000.0 / speed_kmh * 60.0 else: travel dmat[route[k - 1]][node] / 1000.0 / speed_kmh * 60.0 clock travel if clock tw_start[node]: clock tw_start[node] if clock tw_end[node]: return False, node, clock clock service_time return True, -1, clock这里dmat的单位是米除以1000转成公里再除以速度得到小时乘以60转成分钟。早到不是延误把时钟拨到时间窗起点即可晚到则直接返回False调用方就知道该路径不可行。客村站周边路况复杂20公里/小时只是初始值实际应按不同时段调整。午高峰和傍晚峰值的平均速度可以差到五公里每小时以上建议按半小时切片准备速度矩阵。4.2 把时间窗和载重同时塞进合并循环在主循环里不能用单独的total_demand判断也不能只依赖时间窗。正确做法是封装成is_feasible函数合并前同时检查容量、体积、时间窗三个维度。def is_feasible(route, dmat, capacity, service_time2.0, speed_kmh20.0): if total_demand(route) capacity: return False ok, node, clock check_time_window(route, dmat, service_time, speed_kmh) return ok主循环里把if total_demand(new_route) capacity: continue替换成if not is_feasible(new_route, dmat, capacity): continue即可。这里有个容易踩的坑只检查合并后路径总需求不检查单个需求点是否超过容量。输入数据里若混入大件订单独立配送时就已经超载合并后同样不合法。所以is_feasible里可以先遍历route再判断每个demand与capacity的关系。4.3 路网距离替代欧氏距离客村站周边有天桥、单行线和内部道路欧氏距离会低估实际里程。常见做法是用地图API批量获取站点到客户点、客户点之间的驾车或骑行路线距离替换dmat。替换之后节约值公式不变但dij变成路网最短路径长度。需要留意距离矩阵可能不对称站点到客户的上坡路与返程下坡路耗时不同。一个折中方案是把d0i和di0分开保存节约值计算用往返平均距离路线总距离用分段实际距离。数据源单位计算时长适用阶段欧氏距离米毫秒级算法验证、快速演示地图API距离米秒级到分钟级实际调度预计算OD矩阵米分钟级每日批量优化如果需要每天重新优化建议提前把当日客户点之间的OD矩阵算好缓存到本地。客村站覆盖范围不算大OD请求数量在几百到几千之间完全可以在订单截止后几分钟内算完。4.4 参数设置与常见误用容量参数不能直接填车辆铭牌载重要预留15%到20%的余量应对拒收、换货和临时加单。末端电动车常见载重区间在30到60公斤40公斤作为默认值没有脱离实际。速度参数在时间窗检查中影响最大建议用“最慢时段速度”计算宁可让算法给出保守到达时间也不要因为平均速度过快导致晚点。服务时间参数同样要保守城中村每单2分钟是底线送到小区需要上楼时服务时间应提高到5分钟甚至更长。提示时间窗检查里先判断早到还是晚到。晚到直接不可行早到可以等待。不要把等待时间误判为延误。另一个常见误用是把节约里程法的排序结果当成最终路径顺序。节约值只决定合并优先级不决定访问顺序。访问顺序由路径构造过程中“末尾接开头”的规则确定实际执行时还需要用2-opt或手工经验调整尤其涉及单行道时。算法得出一条路径后最好在电子地图上按路径顺序重放一次看是否有明显绕行。5. 验证优化效果和把节约里程法当初始解的进阶用法经过容量和时间窗修正后算法输出已经可以交给调度员试跑。但“看起来合理”和“实际能省”是两件事需要用统一基准验证。推荐把优化前基准定义为“每单一送”的配送方案即每个客户点单独派一辆车从站点出发送达后直接返回总里程等于两倍所有客户点距离累加。优化后总里程是所有车辆路径距离之和优化率等于一减去优化后与基准的比值。baseline sum(2 * dmat[0][i] for i in range(1, len(coords))) optimized sum(total_route_distance(r) for r in routes) improvement 1 - optimized / baseline print(优化率: {:.2%}.format(improvement))只看总里程远远不够还要统计车辆数、最长路线耗时、平均载重利用率。末端配送的瓶颈往往不是总里程而是时间段内的运力分配。比如三条路线总里程很小但都在午高峰出车实际执行时配送员不够就要重新拆分路线。建议输出报告里同时带“每条路线的预计到达时间表”让调度员直接看到哪些订单会在截止时间前送到。5.1 用2-opt给节约里程法做个局部搜索节约里程法构造出来的路径在局部可能仍然存在交叉。对每条路径内部做2-opt优化是成本最低的增强方式。def distance_of(route): return dmat[0][route[0]] sum(dmat[route[k]][route[k 1]] for k in range(len(route) - 1)) dmat[route[-1]][0] def two_opt(route, dmat): improved True while improved: improved False for i in range(len(route) - 1): for k in range(i 1, len(route)): new_route route[:i] route[i:k 1][::-1] route[k 1:] if distance_of(new_route) distance_of(route): route new_route improved True return route2-opt只反转路径内的一段客户点顺序不改变车辆分配。对每条路径跑一遍2-opt后再把新路径放回主流程重新计算总里程和时间窗如果仍可行就替换原路径。这个操作和节约里程法天然互补节约里程法管“哪些点放一辆车”2-opt管“一辆车里怎么排序”。5.2 避免越优化越乱的两个技巧第一个技巧是不要完全按节约值降序合并。末端配送有时间窗时先把时间窗紧迫的订单单独标记在合并循环里对这类订单放开容量上限或者优先执行它们的路线合并避免到了合并后期找不到可用车辆。另一个技巧是单条路径客户点数量不要超过八个。客村站这类城中村片区配送员对门牌号熟悉超过八个点后找路成本会明显上升算法上省下的里程会被寻路时间抵消。把路径长度上限作为一个硬约束加进is_feasible比在目标函数里加惩罚系数更直接。验证时建议在离线地图上把优化前和优化后的路线分别画出来人工看一遍交叉和掉头情况。地图可视化看到的问题往往比数值指标更能说服站长改流程。两三条路线的结果可以直接在Excel里调整订单量变大后再把这套脚本接入每日调度流程。修改容量、速度、服务时间这三个参数时最好保留一份当天订单快照方便后续复盘路线质量。本文还有配套的精品资源点击获取