news 2026/9/13 8:42:15

节约里程法MATLAB例程:贪心算法求解车辆路径问题详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
节约里程法MATLAB例程:贪心算法求解车辆路径问题详解

简介:这是一份基于MATLAB的节约里程法(节约算法)例程包,面向物流配送、车辆路径规划(VRP)问题的学习者与算法入门者。资源仅2个文件,由1个m脚本(核心算法与主程序)和1个xlsx数据文件(客户坐标与需求量)组成,压缩包只有63KB,轻量易上手。例程系统覆盖数据预处理、初始路线生成、节约值计算、迭代优化与结果输出等完整环节,并借助MATLAB矩阵运算和绘图功能直观展示各车辆行驶路径,便于对照理解每次合并路线的距离节省。通过运行主程序,可逐行读懂节约算法的实现逻辑,利用示例数据快速验证,也可替换为自己的客户坐标和需求量进行扩展演练,甚至在此基础上向带时间窗VRPTW、多容量车辆等变种延伸。目前已有319人学习浏览,适合物流工程、运筹优化方向的初学者,以及希望用MATLAB动手实现启发式算法的开发者。

1. 节约里程法:一个贪心算法凭什么仍被当成车辆路径问题的课代表

我第一次在《物流系统规划》课程里看到节约里程法时,觉得这名字听起来绕口,算法逻辑却简单得不像话——按“合并两条配送路线能省多少路程”从大到小一路合下去,本质上是个贪心。后来真正要交一个 matlab 例程作业,四处找压缩包里的代码时才发现,值得注意的从来不是那个公式,而是数据怎么往代码里塞、容量约束怎么判断、路线用什么结构维护。这些细节才是从纸面定理变成能跑通脚本的分水岭。节约里程法至今仍是很多商用运输管理系统生成初始解的算法,也在车辆路径问题和运筹学课程里占着固定位置。本文不猜任何现成压缩包里的内容,只按最常见、最可靠的一套做法,给你一份从读取客户坐标、计算节约值矩阵、并行合并路径到绘制配送路线的完整 matlab 例程,并把参数怎么改、失败时先看哪里一次讲透。

2. 节约里程法的核心:距离矩阵、节约值排序与两种合并策略

实现节约里程法表面上是写合并循环,真正决定代码质量的是三件事:距离矩阵怎么组织、节约值怎么排序、合并时用顺序扫描还是并行扫描。下面把这三件事分开说清。

2.1 距离矩阵为什么必须以配送中心为第 1 行

绝大多数初写节约里程法例程的人,都会先在“距离矩阵怎么构造”上栽跟头。节约里程法的输入不是客户坐标,而是配送中心与客户两两之间的最短路程。配送中心在公式里是基准点,不能把客户坐标矩阵直接拿去做距离计算后就开循环,中心点必须占住一个固定索引位。

常见做法是把配送中心编成 0 号点,客户从 1 编到 n,完整距离矩阵大小为 (n+1)×(n+1)。下面是构造它的最小 matlab 代码:

center = [50, 50]; % 配送中心坐标 coords = center + randn(20, 2) * 10; % 20个客户坐标,围绕中心生成 n = size(coords, 1); allPts = [center; coords]; % 第1行是配送中心 D = zeros(n+1, n+1); for i = 1:n+1 for j = i+1:n+1 D(i,j) = norm(allPts(i,:) - allPts(j,:)); D(j,i) = D(i,j); end end

这段代码里D(i+1,1)就是客户 i 到中心的距离,D(i+1,j+1)是客户 i 到客户 j 的距离。为什么不用pdist2?因为pdist2默认只算客户之间,中心点不在距离矩阵里,后面所有索引都要位移一次,容易乱。把中心放在第 1 行,合并循环里对路径做距离累加时,直接按route(1:end-1)+1取索引,能少写很多调试时间。

提示:如果实际业务用的是路网距离而不是直线距离,这里需要替换成弗洛伊德或 Dijkstra 算出的最短路径矩阵,但矩阵结构和索引逻辑不变。

2.2 节约值矩阵 S(i,j) 与排序列表的三列结构

节约值的定义是:如果把客户 i 和客户 j 分别用两辆车单独配送,总路程是 d(i,0)+d(0,j)。如果合并到同一辆车,直接走 i 到 j,总路程变为 d(i,0)+d(0,j)-d(i,j)。后面的 d(i,j) 就是被“省下来”的那段路程,也就是节约值:

S = zeros(n, n); for i = 1:n for j = i+1:n S(i,j) = D(i+1,1) + D(1,j+1) - D(i+1,j+1); S(j,i) = S(i,j); end end sv = []; for i = 1:n for j = i+1:n sv = [sv; S(i,j), i, j]; % 每行:节约值、客户i、客户j end end sv = sortrows(sv, -1); % 按第一列降序

sv是三列结构:第一列是节约值,第二、三列是客户编号。后面做并行合并时,逐个取sv的行判断能否合并即可。sortrows(sv,-1)表示只对第一列降序,后两列仍按升序,这样节约值相同的候选对会优先选编号小的客户,保持输出稳定。

2.3 顺序式与并行式:matlab 例程里选哪一种合并

合并有两种扫描方式。顺序式先挑一条最大节约值路线,不断向它加客户,直到满载或无法再加入,再换下一条;并行式则是对所有路线同时扫描,每次从全局最大节约值开始判断。两者结果差异明显:

特征顺序扫描并行扫描
路线构造方式一条路线扩展到饱和再取下一条所有路线同时争抢客户
结果倾向路线数偏多、单条路线偏短总里程更低、路线数偏少
matlab 实现需要 while 内层循环,逻辑更绕for 遍历节约值列表即可
适用场景车辆数量紧张,想尽量少出车车辆充足,追求总行驶里程最小

我一般写例程默认用并行扫描,因为它的循环结构和上面排好序的sv列表天然匹配,逻辑也直观。下面一章的完整例程就是并行方式。

3. 可运行的 matlab 例程:从坐标输入、容量约束到路径绘制

这一章给一份在 R2016b 及以上版本都能直接跑的完整脚本,包含了随机数据生成、节约值计算、并行合并、容量检查和最终输出。把坐标和需求量换成你的真实数据就能用于小规模问题。

3.1 主脚本 saving_main.m:最小实现

%% saving_main.m % 节约里程法(并行扫描)matlab 例程 clc; clear; close all; % ---------- 1. 数据准备 ---------- center = [50, 50]; % 配送中心坐标 rng(1); % 固定随机种子,便于结果复现 n = 20; % 客户数 coords = center + randn(n, 2) * 10; % 客户坐标 demand = randi([1, 8], n, 1); % 每个客户需求量 capacity = 40; % 单车最大载重 allPts = [center; coords]; D = zeros(n+1, n+1); for i = 1:n+1 for j = i+1:n+1 D(i,j) = norm(allPts(i,:) - allPts(j,:)); D(j,i) = D(i,j); end end % ---------- 2. 计算节约值并排序 ---------- S = zeros(n, n); for i = 1:n for j = i+1:n S(i,j) = D(i+1,1) + D(1,j+1) - D(i+1,j+1); S(j,i) = S(i,j); end end sv = []; for i = 1:n for j = i+1:n sv = [sv; S(i,j), i, j]; end end sv = sortrows(sv, -1); % ---------- 3. 初始化:每个客户单独一条路线 ---------- routes = num2cell(1:n); % 每个cell是一条路线,初始只装一个客户 loads = demand(:)'; % 每条路线的累计载重 % ---------- 4. 并行合并 ---------- for k = 1:size(sv, 1) i = sv(k, 2); j = sv(k, 3); if sv(k, 1) <= 0 continue; % 节约值为负,合并反而增加里程 end posI = find(cellfun(@(r) ismember(i, r), routes)); posJ = find(cellfun(@(r) ismember(j, r), routes)); if isempty(posI) || isempty(posJ) || posI == posJ continue; % 两客户已在同一路线或已被合并掉 end ri = routes{posI}; rj = routes{posJ}; if ~(ri(1) == i || ri(end) == i) continue; % i 必须是所在路线的端点 end if ~(rj(1) == j || rj(end) == j) continue; % j 必须是所在路线的端点 end if loads(posI) + loads(posJ) > capacity continue; % 容量约束 end % 将 i 翻转到 ri 末尾,将 j 翻转到 rj 开头,再拼接 if ri(1) == i ri = fliplr(ri); end if rj(end) == j rj = fliplr(rj); end routes{posI} = [ri, rj]; loads(posI) = loads(posI) + loads(posJ); routes{posJ} = []; % 原 posJ 路线被合并进 posI end % 删除空路线 routes(cellfun(@isempty, routes)) = []; % ---------- 5. 输出结果 ---------- for r = 1:length(routes) routes{r} = [0, routes{r}, 0]; % 首尾补配送中心 end totalDist = 0; for r = 1:length(routes) fprintf('车辆%d: ', r); fprintf('%d ', routes{r}); fprintf(' 载重%.0f\n', sum(demand(routes{r}(2:end-1)))); for seg = 1:length(routes{r}) - 1 totalDist = totalDist + D(routes{r}(seg)+1, routes{r}(seg+1)+1); end end fprintf('总行驶里程: %.2f\n', totalDist);

num2cell(1:n)生成{1},{2},...,{n},每个 cell 是独立路线;loadsroutes一一对应,这样判断容量时不用重新累加路线需求。合并循环里先做位置查找,再做端点判断和容量判断,三个条件任意不满足就跳过该候选对,这是并行扫描的标准写法。

端点条件为什么要单独判断?因为节约值只是“局部看两点合并省里程”,但实际路径是有方向性的,路线只能从两端接出去。如果客户 i 在路线中间,合并后 i 就变成“三叉路口”,车辆无法按一条直线走完。

3.2 容量约束和端点条件是怎么被验证的

上面的脚本里,容量判断只有一行:loads(posI) + loads(posJ) > capacity。这里需要注意loads必须始终与routes同步更新。初始loads = demand(:)',每次合并后,把被合并路线的载重加到loads(posI),而loads(posJ)不再参与后续比较。有些例程只在最后计算载重,合并过程中不维护,结果会漏掉还没被合并但已经超载的组合。

端点判断的逻辑是:客户 i 必须在它所在路线的首或尾,客户 j 同理。若两者都在端点,通过翻转rirj总能让路径变成“i 在 ri 尾部、j 在 rj 头部”,最后直接拼接,中间就是节约值公式里的 i→j 边:

if ri(1) == i ri = fliplr(ri); end if rj(end) == j rj = fliplr(rj); end

注意fliplr反转的是一个向量,在 R2016b 以后可以直接操作 cell 内的向量。反转后再拼接,路径在逻辑上等价于“车从中心出发,沿着 ri 走到 i,再从 i 开到 j,沿着 rj 回到中心”,没有产生任何需要折返的断点。

3.3 画路径图:判断结果有没有绕路的最快方式

文字输出只能看到路线序列,看不出几何形状。画图是评估节约里程法结果最直观的手段,特别是检查有没有两条路线交叉、某个客户是否被单独扔在外面。

figure; hold on; grid on; axis equal; plot(center(1), center(2), 'kp', 'MarkerSize', 14, 'MarkerFaceColor', 'k'); plot(coords(:,1), coords(:,2), 'bo', 'MarkerSize', 7); for k = 1:n text(coords(k,1)+1, coords(k,2)+1, num2str(k), 'FontSize', 8); end cols = lines(length(routes)); for r = 1:length(routes) x = allPts(routes{r}+1, 1); y = allPts(routes{r}+1, 2); plot(x, y, '-o', 'Color', cols(r,:), 'LineWidth', 1.6); end xlabel('X 坐标'); ylabel('Y 坐标'); title('节约里程法 matlab 例程 -- 车辆路径结果');

allPts(routes{r}+1, :)是把含 0 的路线映射到坐标矩阵:0 对应配送中心,1 对应第一个客户。图中黑色方块是配送中心,彩色线条是不同车辆路径。运行后如果发现两条线路交叉明显,基本可以判断是排序键或合并策略需要调;如果某个客户单独成线,说明它的节约值没能超过容量或端点限制,这类客户在现实中往往需要专车。

4. 例程排错三件事与 2-opt 改良

代码能跑通只是第一步,节约里程法例程真正折磨人的是结果“看起来合理但不够好”。最后一章只讲我最常遇到的三个问题和一种立竿见影的改良手段。

4.1 索引、单位、排序键:例程跑偏的三大来源

第一,索引错位。D(i+1,1)coords(i,:)之间的对应关系一旦写错,节约值全部偏一位,最终路线会在客户编号上“串位”。排错方法是在合并循环前打印D(2,1)norm(coords(1,:) - center)是否相等。

第二,单位不一致。坐标用米,需求量用箱,但车辆里程限制可能用公里。例程里D是坐标直接算的欧氏距离,如果实际限制是 50 公里,坐标却是经纬度差值,容量判断没问题,路线可行性判断就会全部失真。

第三,排序键。默认按节约值降序在多数数据上没问题,但遇到客户沿主干道分布的场景,并行扫描容易把“近距离、方向相反”的两条路线先合并,造成回头路。此时把排序键改成优先合并离中心更远的客户,效果更好。实现很简单,排序前对sv减一个带中心距离的小量:

centerDist = D(2:end, 1); w = 0.3; % 权重,一般取 0.2~0.5 sv(:,1) = sv(:,1) + w * (centerDist(sv(:,2)) + centerDist(sv(:,3)));

4.2 用 2-opt 把例程结果再拧直一圈

节约里程法合并完的路线,常常存在局部绕路:[0, 2, 5, 3, 0]中 5→3 之间如果绕了个锐角,2-opt 能暴力找出反转哪一段能缩短总里程。思路是枚举一条路径内部任意连续区段,反转后计算新路径长度,若更短就保留。对每辆车循环执行到不再变化即可:

function L = pathLen(D, rt) L = 0; for s = 1:length(rt) - 1 L = L + D(rt(s) + 1, rt(s + 1) + 1); end end for r = 1:length(routes) rt = routes{r}; improved = true; while improved improved = false; for a = 2:length(rt) - 2 for b = a + 1:length(rt) - 1 newRt = [rt(1:a-1), fliplr(rt(a:b)), rt(b+1:end)]; if pathLen(D, newRt) < pathLen(D, rt) - 1e-6 rt = newRt; improved = true; end end end end routes{r} = rt; end

pathLen是局部函数,放在脚本末尾,R2016b 之后的 matlab 支持在脚本内定义局部函数。循环里ab都在去掉首尾 0 的区间内活动,避免把配送中心反转进路线中间。2-opt 在这里是纯后处理,不改动节约里程法本身,但通常能把总里程再压掉 5% 到 15%。

实际应用中,时间窗约束可以在合并前单独判断“到达时间是否落在客户允许窗口内”,多车型问题则先按容量大的车型做节约里程法,剩余客户再跑一轮。这些扩展都不需要改动第 3 章例程的总体结构,只往合并条件里加判断即可。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 8:41:37

英文文献检索指南:主流平台与实用技巧

1. 英文文献检索网站概览对于学术研究者、学生和专业人士来说&#xff0c;获取高质量的英文文献是开展研究的基础工作。不同于中文文献检索&#xff0c;英文文献检索需要掌握国际主流的学术数据库和检索技巧。以下是几类常用的英文文献检索平台及其特点&#xff1a;学术数据库类…

作者头像 李华
网站建设 2026/9/13 8:39:25

Java List集合核心特性与性能优化实践

1. Java List集合基础概念与核心特性List是Java集合框架中最基础也最常用的接口之一&#xff0c;它代表一个有序的集合&#xff08;也称为序列&#xff09;。与Set不同&#xff0c;List允许存储重复元素&#xff0c;并且维护了元素的插入顺序。在实际开发中&#xff0c;我们几乎…

作者头像 李华
网站建设 2026/9/13 8:39:04

全桥LLC谐振变换器设计与仿真优化指南

1. 全桥LLC谐振变换器基础原理全桥LLC谐振变换器作为第三代软开关拓扑的代表&#xff0c;其核心优势在于利用谐振腔实现全负载范围内的零电压开关(ZVS)。这种拓扑结构由四个开关管(Q1-Q4)构成的全桥、谐振电感Lr、谐振电容Cr以及励磁电感Lm组成。当开关频率fs接近谐振频率fr时&…

作者头像 李华
网站建设 2026/9/13 8:38:07

阿里开源Agent项目实操:从环境配置到多智能体协作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 8:36:54

HDFS从入门到排障:架构原理、操作实战与高频坑位解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 8:35:36

AIGC技术如何革新游戏开发流程与质量保障

1. AIGC技术如何重塑游戏开发流程AIGC&#xff08;AI Generated Content&#xff09;正在彻底改变游戏行业的传统生产模式。作为从业15年的游戏技术专家&#xff0c;我亲眼见证了从纯手工制作到AI辅助开发的革命性转变。在传统游戏开发中&#xff0c;美术资源、关卡设计、NPC对…

作者头像 李华