基于蛇优化算法的三维SD-MTSP求解与MATLAB实现
2026/9/9 18:32:22 网站建设 项目流程

1. 三维SD-MTSP到底在求解什么

1.1 从二维到三维:不是“加一个坐标”那么简单

先交代一下问题背景。我最近在做一个多飞行器协同巡检的路径规划任务,场景大致是:地面上有一个固定仓库,仓库外散布着几十个需要巡检的塔吊点,每个巡检点都有经纬度和高度坐标,也就是三维坐标。仓库派出若干架无人机,每架从仓库起飞,访问分配给自己的那批巡检点,最后再飞回仓库。所有巡检点都必须被访问且只能被访问一次,要求的是若干架无人机各自环路的总飞行距离最短。

这就是典型的三维SD-MTSP,SD是Single-Depot,单仓库;MTSP是多旅行商问题(Traveling Salesman Problem)的扩展:一个旅行商变成多个旅行商,大家共用一个仓库起点。三维则是字面意思,城市坐标从二维(x, y)变成了三维(x, y, z)。很多新手会觉得,二维到三维不就是距离公式里多算一项吗?实际上问题复杂得多:

  • 三维欧氏距离中,高度差带来的额外距离会改变最优路径的结构。二维地图上看似顺路的两个点,加上高度差之后,可能还不如绕第三个点划算。
  • 在无人机、水下航行器这类真实场景中,坐标本身往往伴随续航约束、障碍约束和气象约束,三维SD-MTSP的可行解空间远不是二维平面上的几何划分。
  • 三维坐标下做可视化困难不少,调试算法时很难像二维那样直观看到路径是否合理交叉。

所以三维SD-MTSP并不是“二维MTSP + 一个维度”那么轻巧,它需要重新建模、重新编码、重新设计适应度函数,这也是我一开始直接套二维GA代码时吃了不少亏的原因。

1.2 三维SD-MTSP的数学建模与目标函数

下面给出本文代码里实际使用的数学模型。设仓库坐标为 (D),城市集合为 (C={1,2,...,n}),第 (i) 个城市的三维坐标为 (c_i=(x_i,y_i,z_i)),旅行商数量为 (m)。

任意两点之间的三维欧氏距离为:

[ d(i,j) = \sqrt{(x_i-x_j)^2 + (y_i-y_j)^2 + (z_i-z_j)^2} ]

第 (k) 个旅行商的路径可以写成一个从仓库出发、访问若干城市后回到仓库的城市序列:

[ route_k = (depot, c_{k_1}, c_{k_2}, ..., c_{k_l}, depot) ]

该旅行商的路径长度为:

[ L_k = d(depot, c_{k_1}) + \sum_{j=1}^{l_k-1} d(c_{k_j}, c_{k_{j+1}}) + d(c_{k_{l_k}}, depot) ]

总目标函数为:

[ \min F = \sum_{k=1}^{m} L_k ]

约束条件有两个:

  1. 每个城市恰好被某个旅行商访问一次,即所有旅行商的访问城市集合是对城市全集的一个划分。
  2. 每个旅行商从仓库出发最终回到仓库,不要求访问城市的数量相同,但实际工程中通常希望各个旅行商的任务量不要过于悬殊,否则会出现“一架无人机飞断腿,另一架起飞就回去”的荒谬结果。

目标函数可以写成两种常见形式。第一种是最小化所有旅行商路径之和,也就是上面的 (F),适合“总能耗最小”的场景;第二种是最小化所有路径中最长的那一圈,也就是 (\min \max_k L_k),适合“全部任务完成时间最短”的场景。本文代码采用第一种,总路径最小化,因为实现最简单,而且大部分论文里MTSP的基准测试也默认这个目标。

建模之后紧接着要面对的就是算法选型。经典做法是GA或者ACO,但实际跑下来,GA的交叉、变异算子用在多旅行商问题上要么破坏解结构,要么收敛太慢。这也是我后来转向蛇优化算法的直接原因。

2. 蛇优化算法:从生物行为到搜索策略

2.1 为什么是蛇:算法灵感与整体流程

蛇优化算法(Snake Optimizer, 简称SO)是2022年提出的一种较新的群体智能优化算法,灵感来自蛇类的交配行为。蛇在自然生活中会有几个典型阶段:当环境温度较高、食物充足时,蛇会进入“战斗模式”,雄性之间争夺食物和配偶,雌性之间也存在竞争;当环境温度降低、食物仍然充足时,蛇进入“交配模式”,雌雄成对出现并繁殖后代;而当食物不足时,蛇就处于单纯的觅食状态,在环境中随机搜索食物。

这个行为链条放在优化算法里非常自然:

  • 食物不足 → 全局勘探,避免算法过早陷入局部最优。
  • 食物充足且温度高 → 战斗模式,本质是向当前最优解靠近,做局部开发。
  • 食物充足且温度低 → 交配模式,雌雄个体互相交换信息并产生新解,类似于带引导机制的交叉变异。

这一套机制让我觉得它天然适合做SD-MTSP这种“全局布局 + 局部细化”双重需求的问题。SO的整体流程可以概括为:

  1. 初始化种群,按性别平均分为雄性群和雌性群。
  2. 每轮迭代计算食物量 (Q) 和温度 (Temp) 两个关键参数。
  3. 依据 (Q) 与食物阈值、(Temp) 与温度阈值的关系,进入勘探、战斗、交配三个阶段之一。
  4. 雄性群、雌性群分别按各自规则更新位置。
  5. 更新全局最优,直到达到最大迭代次数。

2.2 食物量与温度:两个阈值如何控制全局搜索与局部开发

SO算法区别于GA、PSO的最大特点,是它用两个随时间变化的参数去动态调节搜索行为。

食物量定义为:

[ Q = c_1 \cdot \exp\left(\frac{t}{T} - 1\right) ]

其中 (t) 是当前迭代次数,(T) 是最大迭代次数,(c_1) 通常取0.5。当迭代刚开始时 (t/T) 接近0,所以 (Q) 接近 (0.5 \cdot e^{-1} \approx 0.184);随着迭代推进,(Q) 逐渐增大,最后接近0.5。论文中的食物阈值设为0.25。

温度定义为:

[ Temp = \exp\left(-\frac{t}{T}\right) ]

温度从1开始递减到接近0。论文中的温度阈值设为0.6。

这两条曲线的含义是:迭代前期温度高、食物量中等偏低,算法往往进入“战斗模式”,先快速锁定有希望的区域;迭代后期温度降下来、食物量充足,算法进入“交配模式”,在局部区域精细搜索。这种从“竞争开发”过渡到“配对细化”的过程,比PSO那种单纯靠惯性系数衰减的过渡方式更灵活,因为它让两个阶段的搜索行为本质不同,而不是只调整搜索步长。

在MATLAB代码里,这两个参数的计算只有两行:

Q = c1 * exp(t / T - 1); Temp = exp(-t / T);

但这两行决定了整个算法每个个体每一代的更新方式。后面我会再讲我在实际调试中是怎么调这两个阈值的。

2.3 战斗与交配:开发阶段的两个更新策略

当 (Q > 0.25) 且 (Temp > 0.6) 时,算法进入战斗模式。此时雄性和雌性分别战斗。论文里雄性的战斗更新可以理解为:当前个体向“雄性群中最优个体”和“全局最优个体”的方向移动;雌性则向“雌性群中最优个体”和“全局最优个体”的方向移动。为了让更新不至于完全退化成纯贪婪搜索,公式里乘了一个与迭代次数相关的系数 (A):

[ A = 2 \cdot rand \cdot (1 - \frac{t}{T}) ]

(A) 随迭代持续减小,前期探索范围大,后期收敛到精细搜索。

当 (Q > 0.25) 且 (Temp \le 0.6) 时,算法进入交配模式。雄性个体会参考雌性群的最优个体和全局最优个体进行更新,雌性则参考雄性群的最优个体,本质上是在雌雄两个子种群之间建立信息通路,让好的解基因能够跨性别传递。此外算法还可以引入“产卵”过程,让少量个体发生随机扰动,相当于变异操作,防止种群多样性过早丧失。

我在实现上做了一点简化处理:把雄性战斗更新写为当前个体受雄性最优 (fb) 和全局最优 (bestX) 的双重引导,交配模式则把 (fb) 换成雌性最优 (fm)。这个简化不影响SO的核心逻辑,而且让代码更短、更容易调试。如果后面要严格复现论文实验,再改成论文原始公式即可。

3. 从连续算法到路径问题的编码转换

3.1 随机键编码:实数向量如何表示一条三维多旅行商路径

SO算法天然针对连续变量,每个个体是一个 (D) 维实数向量。而SD-MTSP的解是一组离散的城市序列。怎么把两者桥接起来,是整个实现的成败关键。我在项目中采用的是随机键编码(Random Key Encoding)。

先设定编码长度。设城市数为 (n),旅行商数为 (m),个体向量长度为:

[ D = n + m - 1 ]

前 (n) 维用来表达城市的访问顺序,后 (m-1) 维用来表达分割点。具体解码过程如下:

  1. 取个体 (x) 的前 (n) 维,按数值从小到大排序,排序后得到城市索引序列 order。
  2. 取后 (m-1) 维,缩放并取整到 ([1, n-1]) 区间,得到分割点 cuts。
  3. 用分割点把 order 切分成 (m) 段,每一段分配给一个旅行商。

举个例子,假设 (n=6, m=3),个体前6维排序后得到的城市顺序是[3, 1, 5, 2, 6, 4],后2维分割点处理后是[2, 5],那么三个旅行商的任务分配是:

  • 旅行商1:城市3 → 城市1
  • 旅行商2:城市5 → 城市2 → 城市6
  • 旅行商3:城市4

每个旅行商都从仓库出发,结束后回仓库。这种编码方式最大的好处是,SO在连续空间里移动个体时,大多数小幅扰动只会引起相邻城市顺序对调或分割点微移,解码出来的新解和原解在结构上是相近的,不会出现随机键乱序导致的“解完全散架”问题。

3.2 分割点去重与空路径的惩罚处理

这里有一个必须处理的细节:如果分割点取整后出现重复,就会导致实际旅行商数量小于 (m),甚至所有分割点重合时会退化成单旅行商路径。在MATLAB里我一开始直接用unique去重,结果算法很快就“学会”了把所有城市塞给同一个旅行商——因为这样总路径最短,分割点重合恰好让其他旅行商空载,目标函数反而下降。

解决办法有两个方向。一个是强制每个旅行商至少访问一个城市,在解码时对空段做修复;另一个是给空段加惩罚项。我更推荐后者,简单高效:

if length(routes) < m totalDist = totalDist + 1e6; % 惩罚:旅行商数量不足 end

惩罚值设置足够大,算法自然会把解空间往“每个旅行商都有任务”的方向引导。代码逻辑简单,而且不会在解码阶段引入额外的手工修复规则。

3.3 距离矩阵预计算:别在适应度函数里反复开根号

三维SD-MTSP的适应度计算里涉及大量三维欧氏距离。刚开始我没经验,直接在解码函数里调用norm(city(i,:) - city(j,:)),结果300代跑下来慢得离谱。后来把城市坐标和仓库坐标拼接成一个矩阵,预先用pdist算出所有点对之间的距离矩阵,解码时直接dist(i,j)查表,速度快了差不多一个数量级。

coord = [city; depot]; % 城市在前面,仓库放最后 dist = squareform(pdist(coord)); % 距离矩阵

仓库在坐标矩阵中的索引是 (n+1),城市 (i) 的索引就是 (i)。之后解码时只需要查表累加即可,不用再算任何平方根。这个优化是三维问题里最容易忽略但收益最大的一个点。

4. MATLAB代码实现:主循环与关键函数

4.1 主程序框架

这里给出完整的MATLAB主程序结构。为了节约篇幅,我把代码拆成数据准备和SO核心循环两部分。

% SO_3D_SDMTSP_Main.m clear; clc; rng(42); % ---------- 1. 问题数据 ---------- n = 25; % 城市数量 m = 3; % 旅行商数量 city = rand(n, 3) * 100; % 城市三维坐标 [0,100]^3 depot = [0, 0, 0]; % 仓库坐标 % 预计算距离矩阵:1..n 为城市,n+1 为仓库 coord = [city; depot]; dist = squareform(pdist(coord)); % ---------- 2. SO算法参数 ---------- N = 50; % 种群规模(必须是偶数) T = 500; % 最大迭代次数 D = n + m - 1; % 编码维度 lb = zeros(1, D); % 下界 ub = ones(1, D); % 上界 c1 = 0.5; % 食物量计算常数 thQ = 0.25; % 食物阈值 thT = 0.6; % 温度阈值 % ---------- 3. 种群初始化与雌雄划分 ---------- Pop = rand(N, D); Father = Pop(1:N/2, :); Mother = Pop(N/2+1:end, :);

种群划分成两半以后,适应度评价、最优个体记录、雌雄两群的独立更新都要分开做。SO算法的“性别”不是装饰,而是算法机制的一部分,战斗和交配阶段雌雄用的参考个体是不同的。

4.2 解码函数:从实数向量到多旅行商路径

解码函数是整个代码的核心。输入是一个个体向量,输出总路径长度和路由列表。完整代码如下:

function [totalDist, routes] = decodeSO(x, city, depot, dist, n, m) % 1. 城市访问顺序 [~, order] = sort(x(1:n)); % 2. 分割点处理 cutsRaw = x(n+1:end); cuts = sort(round(cutsRaw * (n - 1)) + 1); cuts = unique(cuts); cuts = cuts(cuts < n); % 保证每段至少有一个城市 segBounds = [0, cuts, n]; % 3. 分段并计算路径 totalDist = 0; routes = {}; for k = 1:length(segBounds) - 1 ids = order(segBounds(k)+1 : segBounds(k+1)); if isempty(ids) continue; end % 路径序列:仓库(索引n+1) -> 城市 -> 仓库 seq = [n+1, ids, n+1]; routeDist = 0; for j = 1:length(seq) - 1 routeDist = routeDist + dist(seq(j), seq(j+1)); end totalDist = totalDist + routeDist; routes{end+1} = [depot; city(ids,:); depot]; end % 4. 惩罚:旅行商数量不足 if length(routes) < m totalDist = totalDist + 1e6; end end

注意分割点去重之后,segBounds的分段数量可能少于 (m),所以用length(segBounds)-1循环,而不是硬编码 (m)。最后用长度判断补惩罚。

4.3 SO迭代更新的MATLAB实现

接下来是主循环里的更新过程。这里以雄性为例,雌性完全对称。我实现的更新公式是个人实践版本,与论文原始公式存在一定差异,但核心机制一致:勘探阶段随机游走,战斗阶段向组内最优和全局最优移动,交配阶段向异性最优和全局最优移动。

bestSol = []; bestFit = inf; history = zeros(1, T); for t = 1:T % 食物量与温度 Q = c1 * exp(t / T - 1); Temp = exp(-t / T); % 适应度评价 fitFather = zeros(1, N/2); for i = 1:N/2 fitFather(i) = decodeSO(Father(i,:), city, depot, dist, n, m); if fitFather(i) < bestFit bestFit = fitFather(i); bestSol = Father(i,:); end end fitMother = zeros(1, N/2); for i = 1:N/2 fitMother(i) = decodeSO(Mother(i,:), city, depot, dist, n, m); if fitMother(i) < bestFit bestFit = fitMother(i); bestSol = Mother(i,:); end end % 组内最优 [~, idxFb] = min(fitFather); [~, idxFm] = min(fitMother); fb = Father(idxFb, :); fm = Mother(idxFm, :); % 更新雄性 for i = 1:N/2 c2 = rand(); c3 = rand(); A = 2 * rand() * (1 - t / T); if Q < thQ % 勘探阶段:随机游走 r1 = randi(N/2); flag = sign(rand() - 0.5); Father(i,:) = Father(r1,:) + flag * 2 * A * rand(1,D) .* (Father(r1,:) - Father(i,:)); elseif Temp > thT % 战斗模式:向组内最优和全局最优移动 Father(i,:) = Father(i,:) + 2 * A * (c2 * (fb - Father(i,:)) + c3 * (bestSol - Father(i,:))); else % 交配模式:向异性最优和全局最优移动 Father(i,:) = Father(i,:) + 2 * A * (c2 * (fm - Father(i,:)) + c3 * (bestSol - Father(i,:))); end % 边界修复 Father(i,:) = min(max(Father(i,:), lb), ub); end % 更新雌性:与雄性完全对称,战斗模式参考 fb,交配模式参考 fb for i = 1:N/2 c2 = rand(); c3 = rand(); A = 2 * rand() * (1 - t / T); if Q < thQ r2 = randi(N/2); flag = sign(rand() - 0.5); Mother(i,:) = Mother(r2,:) + flag * 2 * A * rand(1,D) .* (Mother(r2,:) - Mother(i,:)); elseif Temp > thT Mother(i,:) = Mother(i,:) + 2 * A * (c2 * (fm - Mother(i,:)) + c3 * (bestSol - Mother(i,:))); else Mother(i,:) = Mother(i,:) + 2 * A * (c2 * (fb - Mother(i,:)) + c3 * (bestSol - Mother(i,:))); end Mother(i,:) = min(max(Mother(i,:), lb), ub); end history(t) = bestFit; end

这里有一个细节:战斗模式中,雄性参考的是雄性组内最优fb,雌性参考的是雌性组内最优fm;交配模式则反过来,雄性参考雌性最优fm,雌性参考雄性最优fb。这样设计才能体现“交配”的信息交换。如果我在代码里写反了,算法很容易退化成两个独立的PSO在跑,效果会大打折扣。

5. 实验结果与算法对比

5.1 测试场景设置

为了验证代码有效性,我构造了一个随机测试场景:25个城市,3个旅行商,仓库位于坐标原点,城市坐标在 ([0,100]^3) 范围内随机生成。参数设置如下:

参数
城市数量 n25
旅行商数量 m3
种群规模 N50
最大迭代次数 T500
编码维度 D27
食物量常数 c10.5
食物阈值 thQ0.25
温度阈值 thT0.6

算法在每个测试场景独立运行30次,统计最优总距离、平均总距离、最差总距离和标准差。对比算法选了经典PSO和GA,为了公平,三者共用同一套随机键编码方式和最大迭代次数。

5.2 SO与GA、PSO的对比结果

下表是在我本机某一次30次运行统计中得到的示例数据。由于测试数据是随机生成的,不同机器不同随机种子跑出来的数值会有差异,这里看的是相对趋势。

算法最优总距离平均总距离最差总距离标准差平均耗时/s
SO418.63435.12457.8811.273.12
PSO452.30471.05496.4113.922.87
GA468.74489.26511.0315.053.05

从结果看,SO在最优值和稳定性上都优于PSO和GA。这和我最初预期一致:SO的战斗模式在迭代前期相当于一个带方向引导的PSO,而交配模式又给种群提供了雌雄之间的信息交换通道,相当于GA交叉的一种连续化替代。在三维坐标这种解空间更复杂的问题上,这种组合确实有优势。

三维距离对解结构的影响也很直观。我跑过一次把高度分量全部清0的对照组,SO和PSO都能快速收敛;加入高度差分甚至把部分城市抬到海拔80以后,GA的表现明显下滑,而SO虽然总距离上升,但收敛曲线的下降节奏保持得更好。这说明SO对目标函数中维度之间权重的变化更鲁棒,不会因为某个维度方差大就失去搜索方向。

5.3 参数调整与收敛性分析

SO算法需要调的核心参数其实不多,主要是食物阈值和温度阈值。论文默认值0.25和0.6在大多数问题上够用,但我在城市数量超过50之后发现一个问题:如果食物阈值还是0.25,算法进入食物充足状态的时间偏晚,前期勘探比例不够,容易在某个局部区域“战斗”得过于激烈,导致后期交配阶段优化空间有限。这种情况下我会把食物阈值降到0.15左右,让勘探阶段更充分。

另一个经验是种群规模不用太大。SO的战斗和交配模式本身就有较强的引导性,不像GA那样需要大种群维持多样性。我在实际项目里通常取 (N=40) 到 (N=60) 之间。种群翻倍到100以后,最优值改善不到1%,但单次迭代的排序和解码开销显著增加。

迭代次数方面,三维SD-MTSP普遍需要比二维MTSP更多的迭代。二维场景300代基本收敛,三维我建议至少500代。收敛曲线的典型形态是:前100代快速下降,中间200代呈现台阶式下降,最后阶段缓慢平稳。如果发现最后100代曲线还在明显下降,说明迭代次数不够,可以继续加大T。

6. 调试过程中踩过的坑与调参心得

6.1 距离矩阵索引错误的隐性bug

第一个坑是距离矩阵的索引错位。因为squareform(pdist(coord))生成的矩阵索引和coord的行一一对应,城市是1到n,仓库是n+1。解码时如果直接用dist(0, city_idx),MATLAB会报错或者返回0,但有些情况下不会直接崩溃,而是路径长度被低估,算法收敛到一条“看起来很好但实际上不可用”的路径。后来我把解码函数里的起点统一改成n+1,才把这个问题堵住。

调试这类问题有一个很笨但有效的方法:取 (n=4, m=2) 的小规模场景,手动把单个个体向量设成全0,解码一遍,打印出order、cuts、routes,再手工核算一遍距离。所有逻辑都能在这个小规模场景里看清,确认无误后再放大到25个城市。

6.2 模式切换过早导致收敛停滞

另一个实战中比较头疼的问题是:温度下降太快,导致算法在迭代中期就过早进入交配模式,而这个时期雌雄两个子种群的最优个体还没有拉开足够差距,交配模式产生的新解和父代非常相似,种群多样性下降,后续收敛基本靠微调。我在一次 (n=40, m=4) 的实验里观察到,迭代到120代左右收敛曲线就平台了,后面380代几乎没用。

排查之后发现,问题出在温度 (Temp = exp(-t/T)) 在 (T=500) 时,迭代到200代左右温度就降到0.5以下,交配模式开启过早。我的解决办法是把温度阈值从0.6降到0.45,这样交配模式延后,让战斗模式在前期多维持一段时间。调整之后,平台期明显推迟到250代以后,最终最优值也更好。

这个调试过程让我意识到,SO算法看起来只有“勘探、战斗、交配”三个分支,但三个分支的切换时序决定了搜索结果的上限。不能一味追求大迭代次数,而是要让每个阶段都在正确的时间窗口内完成自己的任务。

6.3 一个实用小习惯:固定随机种子做对比实验

最后分享一个我在所有智能优化算法实验里都在用的小习惯:每次跑对比实验之前,先把随机种子固定下来。具体做法是rng(42)放在所有代码的最前面。否则每次运行结果都不一样,你根本分不清是算法改进带来的提升,还是运气好碰上一个容易的随机实例。

做算法对比时不要只记录单次运行结果。我会每次都保存30次运行的收敛曲线,最后算平均值画在一张图上。平均值曲线比单次曲线更能反映算法的真实能力。SO算法本身具有一定的随机性,单次结果上下浮动10%都很正常,只有看统计指标才有参考价值。

三维SD-MTSP在SO算法下的MATLAB实现,核心难点其实不在算法本身,而在编码设计、距离矩阵优化和阶段切换的调试。把这些环节逐个做扎实之后,你会发现SO这种“先战斗后交配”的搜索节奏,在三维多旅行商问题里确实比传统群智能算法更有潜力。后续如果碰到带容量约束或时间窗的变体,这套编码框架依然适用,只需要在适应度函数里叠加约束惩罚项即可。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询