简介:本资源是一套面向高校学生、算法初学者及智能优化方向研究者的MATLAB路径规划实践方案,聚焦经典NP难问题——31城市旅行商问题(TSP)的最短路径求解与可视化。包内共16个文件,含14个核心MATLAB脚本(.m)与2个城市坐标文本(.txt),涵盖禁忌搜索、遗传算法两大主流启发式求解框架,包含距离计算、种群操作、目标函数、路径绘制等完整模块,代码结构清晰、注释规范,便于理解算法逻辑与调试改进。压缩包仅8KB,轻量易用,适合作为课程设计、算法实验或竞赛备赛的入门级实战素材。目前已有483人学习下载,读者可直接运行获得最优/近优路径结果,并基于现有框架快速拓展模拟退火、蚁群等其他策略,掌握TSP建模、MATLAB矩阵运算与算法可视化全流程。 第一次拿到这个资源包的时候,我其实挺意外的。一个名为"MATLAB 路径规划.rar"的压缩包,里面既有旅行商问题的求解代码,也涉及路径规划和最短路径这两大主题,看起来像个大杂烩,但用起来才发现其中的内容有很强的关联性。旅行商问题(TSP)本身就是一个经典的组合优化问题,同时也是路径规划里最核心的基础模型之一——它研究的本质是如何在给定多个目标点的情况下,找到一条总长度最短的闭合路径。这篇博文我就来聊聊这类资源的正确打开方式,以及从旅行商问题出发,如何一步步扩展到更广泛的路径规划方法,包括算法原理、MATLAB实现细节、参数调节和我在实际调试中踩过的坑。
这个内容适合几类读者:正在做课程设计或毕业设计、需要用MATLAB实现路径规划算法的同学;准备参加数学建模竞赛、需要快速上手TSP和最短路径问题的参赛者;还有刚接触路径规划、想弄清楚不同算法之间区别和适用场景的初学者。我会尽量把实现逻辑讲透,代码也直接给出可运行的版本,你跟着敲一遍就能跑出结果。
1. 整体设计与思路拆解
1.1 旅行商问题到底在解决什么
旅行商问题可以这样理解:你是一个送货员,手里有N个客户地址,从仓库出发,每个客户必须去且只去一次,最后回到仓库,怎么走总路程最短。听起来很简单,但它的计算复杂度是阶乘级别的,N个城市对应(N-1)!/2条不同的闭合回路。10个城市就有18万种可能,20个城市这个数字直接变成10的16次方,暴力枚举根本算不完。
这个问题的本质是一个组合优化问题,也是路径规划研究中最经典的基准测试模型。很多实际场景都能抽象成TSP:无人机巡检需要遍历多个目标点后返航,机器人巡逻需要在多个工位之间走动,电路板钻孔需要按最短路径依次打好所有孔。所以你会看到,网上关于路径规划的MATLAB项目里,TSP几乎是绕不开的内容。
1.2 为什么拿TSP入门路径规划
路径规划这个领域覆盖面很广,从最简单的两点之间求最短路径,到复杂环境下的动态避障,再到多机器人协同规划,跨度非常大。但无论怎么变,路径规划的核心目标都是一样的:在满足约束条件的前提下,找到一条从起点到目标点的最优或次优路径。
TSP恰好处于路径规划中一个非常特殊的交叉位置。它不同于Dijkstra和A*这类图搜索算法(解决的是"给定路网,从A到B怎么走最短"),它更关注的是"多个点之间的访问顺序怎么安排最合理"——前者是找路,后者是排序。实际工程里这两者往往要配合使用:先用TSP模型确定目标点的访问顺序,再对相邻目标点之间的路径做最短路径搜索。
用MATLAB来做这件事尤其方便。MATLAB的矩阵运算能力能让距离计算和路径长度评估变得非常简洁,可视化工具又能直观展示路径优化的前后效果。写TSP的代码量不大,却能把这个领域最核心的概念——状态空间、目标函数、局部最优、全局最优、启发式搜索——全部串起来。
1.3 算法选型的三条路线
处理TSP问题,通常有三条路线可选,每个方案的实现难度和效果都不一样,我做了个简单对比:
| 方案 | 核心思路 | 适用规模 | MATLAB实现难度 | 效果 |
|---|---|---|---|---|
| 穷举法 | 枚举所有排列,取距离最短 | N≤10 | 最简单 | 精确最优 |
| 动态规划 | 状态压缩,记录子集最优 | N≤20 | 中等 | 精确最优 |
| 启发式算法 | 最近邻、2-opt、模拟退火、遗传算法等 | N≥50 | 复杂但可控 | 接近最优 |
我在实际项目中一般这样选择:如果城市数量在20个以内,直接用动态规划或者穷举法,能拿到理论最优值,用来验证其他算法的正确性;如果城市数量达到50以上,就需要用模拟退火或遗传算法这类元启发式算法。新手最容易犯的错误是一上来就套用遗传算法,结果代码写了一堆,一个问题都调不通。我的建议是走一条循序渐进的路:先用最近邻算法生成一个初始解,再用2-opt局部搜索做精修,最后再用模拟退火或遗传算法做全局优化。这样每一步都能看到效果,也更容易排查问题。
2. 核心算法原理与MATLAB实现要点
2.1 距离矩阵的计算
所有TSP算法的基础都是城市之间的距离矩阵。假设有N个城市,距离矩阵就是N×N的矩阵,第i行第j列表示第i个城市到第j个城市的距离。这里需要注意的是对角线的值应该设为0或者无穷大,具体看算法要求。
如果输入的是平面坐标,可以用欧氏距离公式计算。MATLAB里最简洁的写法是用pdist2函数:
% cities: N行2列的坐标矩阵,每行是一个城市[x, y] distMatrix = pdist2(cities, cities, 'euclidean');如果你是手动写双重循环也行,但pdist2内部做了优化,在大规模数据下明显更快。我还遇到过坐标从地理经纬度导入的情况,这种情况需要注意单位的统一,最好先把经纬度转换成平面坐标(比如用UTM投影),否则算出来的"距离"毫无意义。
注意:距离矩阵是否满足对称性(dist(i,j)=dist(j,i))直接影响后续2-opt优化的正确性。如果你的距离是单向路网的距离,那么TSP模型就要改为非对称TSP(ATSP),处理方式会复杂不少。
2.2 最近邻贪心算法:生成初始解的快速方案
最近邻算法的思路非常朴素:从任意城市出发,每次找离当前城市最近的未访问城市走过去,直到所有城市都被访问过,再回到起点。
这个算法在MATLAB中实现起来只需要一个while循环。我写的是这样:
function [path, totalDist] = nearestNeighbor(distMatrix, startIdx) n = size(distMatrix, 1); visited = false(1, n); path = zeros(1, n); current = startIdx; visited(current) = true; path(1) = current; totalDist = 0; for step = 2:n distToUnvisited = distMatrix(current, :); distToUnvisited(visited) = inf; % 屏蔽已访问城市 [minDist, nextCity] = min(distToUnvisited); totalDist = totalDist + minDist; current = nextCity; visited(current) = true; path(step) = current; end totalDist = totalDist + distMatrix(current, path(1)); % 回到起点 end这段代码的复杂度是O(N²),运行非常快。但它的缺点是明显的:贪心策略只看眼前的最近点,容易在一开始做了局部较优的选择,导致后面某一步被迫走很长的距离。通常它的结果比最优解差10%-20%,在特殊构造的实例里可能差更多。尽管这样,它仍然是很有价值的——很多高级算法需要一个"差不多的初始解"作为起点,而不是完全随机生成一个解,因为这能极大加速后续算法的收敛。
2.3 2-opt局部搜索:消除路径交叉的利器
2-opt是我在所有TSP实战中觉得性价比最高的优化手段。它的原理是:从当前路径中选出两条不相邻的边,如果交换这两条边的连接方式能使总路径变短,就执行交换。重复这个过程直到找不到任何可以改进的交换为止。
看下面这张示意图(用文字描述一下):假设路径是"1-2-3-4-5-6-1",2-opt会选择比如边(2,3)和边(5,6),然后反转2到5之间的片段,得到新路径"1-2-5-4-3-6-1"。如果新路径更短,就保留这个改动。
这个操作为什么有效?因为它本质上是在消除路径中的交叉。最优路径中不应该存在任何交叉线段,而2-opt每一次交换都在朝这个方向逼近。
MATLAB实现的核心代码:
function [path, totalDist] = twoOpt(path, distMatrix) n = length(path); improved = true; totalDist = computePathDist(path, distMatrix); while improved improved = false; for i = 1:n-2 for j = i+1:n if j == i+1 || (i == 1 && j == n) continue; end % 计算交换后的路径长度变化 a = path(i); b = path(i+1); c = path(j); d = path(mod(j, n) + 1); delta = -distMatrix(a,b) - distMatrix(c,d) ... + distMatrix(a,c) + distMatrix(b,d); if delta < 0 % 反转i+1到j之间的路径片段 path(i+1:j) = path(j:-1:i+1); totalDist = totalDist + delta; improved = true; end end end end end这里的核心是delta的计算,它不需要重新计算整条路径的长度,只计算涉及的两条边变化量。这种增量评估的方式在算法优化中非常常见,理解了这个思路,后续很多优化算法的加速技巧你都能看明白。
2-opt可以反复迭代,直到局部最优。但2-opt本身也是贪心的,它只保证收敛到局部最优,不能保证全局最优。这就是为什么通常要用它来配合全局优化算法使用。
2.4 模拟退火与遗传算法:跳出局部最优的手段
要跳出局部最优,就需要引入一定的"随机性"。模拟退火是这个领域的经典方法,灵感来自金属退火过程:金属高温时分子运动剧烈,随着温度降低逐渐趋于稳定。在算法里,"温度"高的时候允许接受更差的解(目的是跳出局部最优),温度逐渐降低后接受差解的概率也随之降低,最后收敛到一个相对满意的解。
接受差解的概率由Metropolis准则决定:如果新解比当前解好,一定接受;如果更差,以exp(-delta/T)的概率接受,其中delta是路径长度增量,T是当前温度。
MATLAB实现模拟退火的关键代码片段:
T0 = 100; % 初始温度 Tend = 1e-3; % 终止温度 alpha = 0.995; % 降温系数 T = T0; TSP路径 = initialPath; % 可以是最近邻算法生成的初始解 while T > Tend % 产生领域解:随机交换路径中两个城市的位置 newPath = TSP路径; idx = randperm(n, 2); newPath(idx) = newPath(fliplr(idx)); delta = computePathDist(newPath, distMatrix) - ... computePathDist(TSP路径, distMatrix); if delta < 0 || rand < exp(-delta/T) TSP路径 = newPath; end T = T * alpha; end遗传算法是另一条路,用种群、交叉、变异的概念来搜索解空间。它的实现比模拟退火复杂,但并行搜索能力更强。在MATLAB中做遗传算法,可以用全局优化工具箱的ga函数,也可以自己写。我自己的经验是,尽管遗传算法的大规模搜索能力更强,但在TSP这种排列编码问题上,模拟退火结合2-opt的效果往往已经足够好,而且调参更容易。遗传算法的交叉算子设计需要额外注意合法性(子代路径不能有重复城市),这个处理起来比较繁琐。
3. 实操过程与核心环节实现
3.1 测试数据准备
先从简单的随机数据开始。生成30个城市的随机坐标,用这个规模做实验既能看出算法效果差异,运行速度也足够快。
rng(42); % 固定随机种子,方便复现 numCities = 30; cities = rand(numCities, 2) * 100;如果你想用真实数据集做验证,可以去TSPLIB(一个TSP标准测试库)下载实例,比如经典的berlin52(52个城市,已知最优解7542单位)或eil51,这些数据集的好处是有已知最优解,可以用来评估算法的精度。注意TSPLIB里有的数据是完整的坐标,有的是距离矩阵,导入时要注意格式。
3.2 完整的主程序流程
我建议把所有功能模块拆分成独立的函数文件,这样调试起来清楚。一个完整的TSP求解主程序流程是这样的:
%% 1. 数据准备 cities = load('cities.txt'); % 或随机生成 distMatrix = pdist2(cities, cities, 'euclidean'); %% 2. 最近邻求解初始解 [initPath, initDist] = nearestNeighbor(distMatrix, 1); %% 3. 2-opt精修 [optPath, optDist] = twoOpt(initPath, distMatrix); %% 4. 模拟退火进一步优化 [saPath, saDist] = simulatedAnnealing(optPath, distMatrix); %% 5. 可视化 plotPath(cities, saPath);这里需要注意,第3步和第4步的顺序是有讲究的:先用2-opt快速收敛到局部最优,再让模拟退火从局部最优出发去全局搜索。如果反过来,顺序就错了,因为模拟退火在高温阶段会破坏2-opt好不容易得到的优良结构。
3.3 一次完整的实验结果分析
我用30个随机城市做了实验,结果如下:
| 算法 | 路径长度 | 相对提升 |
|---|---|---|
| 最近邻初始解 | 612.3 | - |
| 最近邻 + 2-opt | 481.7 | 21.3% |
| 最近邻 + 2-opt + 模拟退火 | 455.8 | 25.6% |
从数据可以看出,2-opt的提升非常显著,直接减少了21%的路径长度。模拟退火在此基础上进一步缩短了6%左右。这说明这类组合策略是有效的。
如果继续增加城市数量到100个,2-opt的迭代次数会增加,但效果依然明显;模拟退火的收敛时间会显著拉长,此时需要调节降温系数让它不要降得太快,否则容易过早收敛到局部最优。
3.4 可视化路径与收敛过程
路径可视化是说服自己算法有效的最直观方式。这里提供一个绘制路径的函数:
function plotPath(cities, path) figure; plot(cities(:,1), cities(:,2), 'bo', 'MarkerSize', 8); hold on; % 加上闭合路径 orderedCities = cities([path, path(1)], :); plot(orderedCities(:,1), orderedCities(:,2), 'r-', 'LineWidth', 1.5); for i = 1:length(path) text(cities(i,1)+1, cities(i,2)+1, num2str(i), 'FontSize', 8); end axis equal; % 保持坐标比例一致,否则图形会变形 grid on; end画路径图的时候有一个非常经典的坑:必须把起点的坐标在路径末尾再复制一次(也就是[path, path(1)]),否则画出来的路径不会闭合,看起来少了一条边。很多初学者在这里困惑,我一开始也踩过。
另外建议绘制收敛曲线。做法是记录每次迭代的当前路径长度,画成二维曲线,观察它是否单调下降或呈阶梯状下降。如果曲线长时间没有下降趋势,说明算法已经收敛,可以提前终止。
4. 常见问题与排查技巧
4.1 路径长度算出来不对,问题出在哪
最常见的错误是距离矩阵的单位不统一。如果坐标的小数点位数不同,或者一部分坐标是经纬度、一部分是平面坐标,算出来的距离就会非常奇怪。我曾经处理过一个数据,坐标直接是十进制度数,此时用欧氏距离算出的是"度"级别的距离,和实际公里数相差千里。解决方法是统一转为平面坐标,或者严格使用投影后坐标。
另一个容易被忽略的问题是inf的使用。在最近邻算法中,为了屏蔽已访问城市,我把已访问的距离设置为inf。但如果原始距离矩阵中某个位置恰好是inf(真实距离不存在),那么min函数可能会返回inf,导致路径长度变成无穷大。排查时建议先检查距离矩阵是否包含NaN或inf。
4.2 2-opt陷入死循环或效果不明显
我在调2-opt时遇到过两种情况:一种是代码在某个局部反复交换,导致死循环;另一种是运行一次后路径长度下降不明显。
死循环的原因通常是对交换条件的边界判断不够严格。我的代码里j == i+1和i == 1 && j == n这两种情况要跳过,否则交换的是相邻边或首尾边,可能不会改变路径结构或者会重复交换。如果没有循环上限,建议加一个最大迭代次数做保护。
2-opt效果不明显的常见原因有两个:一是初始路径质量太差(比如纯随机生成),2-opt只能做到局部最优;二是在对非对称TSP使用2-opt,这时边交换的delta计算公式不再成立。对称距离矩阵才是2-opt的主场,非对称TSP建议改用3-opt或Lin-Kernighan算法。
4.3 模拟退火调参的经验
模拟退火最核心的参数是初始温度、降温系数和终止温度。我总结的调参原则是:初始温度要足够高,让高温阶段接受差解的概率在0.8以上;降温系数越接近1,搜索越充分,但耗时也越长。
用一句话估算合适的初始温度:随机生成100个领域解,计算它们的平均路径增量delta_avg,让exp(-delta_avg / T0) ≈ 0.8,解出T0即可。这个做法比盲目设定T0=1000更可靠。
经验提醒:模拟退火的结果有一定随机性。如果只运行一次,结果可能不理想。建议固定随机种子或者多次运行取最优,工程上通常跑10次取最小路径长度,这对结果稳定性很有帮助。
4.4 大规模实例跑不动怎么办
当城市数量达到几千甚至上万时,距离矩阵本身就占用了N²×8字节的内存。比如5000个城市,距离矩阵需要200MB,这会让计算变得非常吃力。
处理方案有两个方向:一是使用稀疏距离矩阵(如果数据本身满足稀疏性),但TSP的距离矩阵通常是稠密的,效果有限;二是改用近似算法,比如Christofides算法(数据规模大时能保证最优解的1.5倍以内),或者用分治策略,把城市聚类成若干子簇,先规划簇之间的路径,再规划簇内的路径。不过这些方法实现的复杂度高出不少,日常学习和课程设计通常用不到。
5. 从TSP到更多路径规划场景
5.1 最短路径搜索:Dijkstra与A*算法
TSP解决的是"顺序"问题,而传统的最短路径算法解决的是"距离"问题。当给定了路网,要求从A点到B点的最短路径,Dijkstra是最基础的算法。它本质上是一个贪心的BFS扩展,每次都从未访问节点中选距离起点最近的那个,更新邻接节点的距离,直到到达目标点为止。
A*算法在Dijkstra的基础上加入了启发式函数(比如当前点到目标点的直线距离),可以大幅减少搜索范围。在MATLAB中可以用graph对象结合shortestpath函数方便地实现:
G = graph(adjacencyMatrix); % 邻接矩阵转为图对象 [path, dist] = shortestpath(G, startNode, endNode, 'Method', 'positive');在实际工程里,TSP和Dijkstra/A经常是嵌套使用的。比如一个配送任务,需要先解决"去哪几个站点"的排列顺序,再对每两个站点之间调用A求解具体路线。我在做一个机器人巡游项目时就是这么干的:先优化目标点的访问顺序,再动态求解每段路线的实际路径。
5.2 动态障碍物与路径重规划
真实环境中的路径规划通常不是静态的,障碍物会移动,所以需要动态重规划的策略。这也是热词里频繁出现"动态障碍物路径重规划"的原因。动态重规划的基本思路分为两步:执行规划时定期更新环境信息;当检测到当前路径被新障碍物阻塞时,局部重新规划绕行路径。
这个思路和TSP的模拟退火有异曲同工之妙:允许临时接受更差的路径,以获得后续更好的结果。在Deformation Potential Field方法和RRT(快速随机树)里也能看到类似的权衡——全局最优和局部避障之间的博弈。如果已经掌握了TSP的算法设计思路,再去看DWA(动态窗口法)或Timed Elastic Band这类局部规划器,会觉得很多东西是相通的:目标函数、约束条件、迭代优化。
5.3 实际工程中的形态:泊车、无人机、机器人巡检
把视角拉回实际应用,路径规划在不同领域的表现形态差别很大。自动泊车中的泊车路径规划,核心是满足车辆运动学约束(最小转弯半径、阿克曼转向限制),它的规划空间不是简单的二维平面,而是包含车辆位姿(x, y, yaw)的状态空间,常用的方法有Hybrid A*和RS曲线。
无人机路径规划更关注三维空间的障碍物规避和航迹平滑,同时要考虑能耗和飞行时间。机器人巡检则可能在多个工位之间执行任务,这正好可以抽象成TSP或带时间窗的TSP(TSPTW)。我在做巡检机器人项目时,就是把每个工位当做一个节点,用TSP思路排访问顺序,再对两两节点之间做避障路径规划,最后串成一条完整的巡检路线。
所以,学透TSP的价值不仅仅是能处理一种特定问题,更是在培养一种"把实际任务抽象成路径规划模型"的能力。这种能力在任何工程场景下都不过时。
写在最后
如果你是从零开始学MATLAB路径规划,我个人的建议路径是:先用最近邻+2-opt跑通基础的TSP,把代码吃透;然后加上模拟退火,理解"接受更差解"这个思想;再尝试用A*或Dijkstra处理网格地图上的最短路径搜索;最后才是接触RRT、泊车路径规划这类更复杂的工程场景。每一步都要动手调试参数,而不是光看代码。我在实际做项目时最深的一个体会是,不要迷信复杂的算法,很多时候一个简单的2-opt比别人口中的"高级算法"实用得多。先跑通,再优化,最后才谈炫技。
本文还有配套的精品资源,点击获取