一、目的
旅行商问题(Traveling Salesman Problem, TSP)是经典的 NP-Hard 组合优化问题:给定 N 个点,求经过所有点恰好一次的最短闭合/开放路径。
二、效果

三、实现
a、整体算法流程
cpp
输入 N 个 3D 点
│
▼
① 最近邻启发式(Nearest Neighbor) ── 生成「还不错」的初始解
│ start>=0 时从指定点出发跑;否则随机抽 20 个候选起点选最好的
▼
② 2-opt 局部搜索 ── 反复做「两条边交叉→替换成两条更短的边」直到无改进
│ closedLoop 决定首尾是不是一对允许交换的边
▼
③ (可选) 3-opt 局部搜索 ── 一次切三条边,4 种重组方式选最优
│ 点多时耗时,所以默认 n ≤ 300 才启用
▼
④ 开放路径优化:
├─ 先求闭合回路的 2/3-opt 最优
└─ 找到闭合路径中「最长的一条边」(breakEdge),在此处打断
→ 这样起点 S = breakEdge 后点,终点 E = breakEdge 前点开放路径天然就最短!
b、2-opt 原理
2-opt 是 TSP 最经典的改进算法:在路径中找到两条边,如果交叉了或者「新方式连接更短」,就把这两条边切断,按另一种方式重新接起来:
cpp
原路径:A → B → ...... → C → D
两条边:(A-B) + (C-D) 长度 = d(A,B) + d(C,D)
剪断后:(A-C) + (B-D) 长度 = d(A,C) + d(B,D)
如果后者更短 → 把 B~C 这段整个 flip(翻转顺序)!
c、3-opt 原理
2-opt 只能处理 2 条边的交换,3-opt 会同时切 3 条边,然后用 4 种重连方式挑最短的:
cpp
原路径:[A-B]...[C-D]...[E-F] 边 = (A-B) (C-D) (E-F)
case1:翻转 B~C 段 → (A-C)(B-D)(E-F)
case2:翻转 D~E 段 → (A-B)(C-E)(D-F)
case3:把 C~F 段和 B 段重排拼接 → (A-D)(E-B)(C-F)
case4:reverse + 拼接 → (A-F)(C-D)(E-B)
4 种中选「替换后总长度最短」的那种
四、重点代码讲解
a、最近邻代码
cpp
QVector<int> TSPAlgo::nearestNeighbor(const QVector<Point>& points,
int startIdx) {
int n = points.size();
if (n == 0) return {};
auto runFrom = [&](int start) -> QVector<int> {
QVector<int> cur;
cur.reserve(n);
QVector<bool> vis(n, false);
cur.push_back(start);
vis[start] = true;
for (int step = 1; step < n; ++step) {
int last = cur.back();
double minD = std::numeric_limits<double>::max();
int next = -1;
for (int k = 0; k < n; ++k) {
if (!vis[k]) {
double d = points[last].distTo(points[k]);
if (d < minD) {
minD = d;
next = k;
}
}
}
if (next != -1) {
cur.push_back(next);
vis[next] = true;
}
}
return cur;
};
if (startIdx >= 0 && startIdx < n) return runFrom(startIdx);
double bestLen = std::numeric_limits<double>::max();
QVector<int> best;
int searchCount = qMin(n, 20);
// 随机种子,固定随机种子可以让在相同数组情况下,随机出来的值一样的
std::mt19937 rng(42);
QVector<int> candidates(n);
std::iota(candidates.begin(), candidates.end(), 0);
std::shuffle(candidates.begin(), candidates.end(), rng);
candidates.resize(searchCount);
for (int s : candidates) {
QVector<int> cur = runFrom(s);
double len = tourLength(points, cur, false);
if (len < bestLen) {
bestLen = len;
best = cur;
}
}
return best;
}
b、2-opt代码O(n²)
cpp
bool TSPAlgo::tryTwoOptSwap(const QVector<Point>& points, QVector<int>& tour,
bool closedLoop, double& currentLen) {
int n = tour.size();
if (n < 4) return false;
bool improved = false;
int iMax = closedLoop ? n : n - 1;
for (int i = 0; i < iMax; ++i) {
int jMin = i + 2;
int jMax = closedLoop ? ((i == 0) ? n - 2 : n - 1) : n - 1;
if (jMin > jMax) continue;
int a = tour[i];
int a_next = tour[(i + 1) % n];
for (int j = jMin; j <= jMax; ++j) {
if (!closedLoop && i == 0 && j == n - 1) continue;
int b = tour[j];
int b_next = tour[(j + 1) % n];
double before =
points[a].distTo(points[a_next]) + points[b].distTo(points[b_next]);
double after =
points[a].distTo(points[b]) + points[a_next].distTo(points[b_next]);
double delta = after - before;
if (delta < -1e-12) {
std::reverse(tour.begin() + i + 1, tour.begin() + j + 1);
currentLen += delta;
improved = true;
}
}
}
return improved;
}
c、3-opt代码O(n³)
cpp
bool TSPAlgo::tryThreeOptSwap(const QVector<Point>& points, QVector<int>& tour,
bool closedLoop, double& currentLen) {
int n = tour.size();
if (n < 6) return false;
bool improved = false;
for (int i = 0; i < n; ++i) {
for (int j = i + 2; j < n; ++j) {
for (int k = j + 2; k < n; ++k) {
if (i == 0 && k == n - 1 && !closedLoop) continue;
int a = tour[i], b = tour[(i + 1) % n];
int c = tour[j], d = tour[(j + 1) % n];
int e = tour[k], f = tour[(k + 1) % n];
if (!closedLoop && ((i + 1) % n == 0 || (j + 1) % n == 0)) continue;
if (!closedLoop && (k + 1) % n == 0) continue;
double original = points[a].distTo(points[b]) +
points[c].distTo(points[d]) +
points[e].distTo(points[f]);
double case1 = points[a].distTo(points[c]) +
points[b].distTo(points[d]) +
points[e].distTo(points[f]);
double case2 = points[a].distTo(points[b]) +
points[c].distTo(points[e]) +
points[d].distTo(points[f]);
double case3 = points[a].distTo(points[d]) +
points[e].distTo(points[b]) +
points[c].distTo(points[f]);
double case4 = points[f].distTo(points[b]) +
points[c].distTo(points[d]) +
points[e].distTo(points[a]);
double bestCase = original;
int caseType = 0;
if (case1 < bestCase) {
bestCase = case1;
caseType = 1;
}
if (case2 < bestCase) {
bestCase = case2;
caseType = 2;
}
if (case3 < bestCase) {
bestCase = case3;
caseType = 3;
}
if (case4 < bestCase) {
bestCase = case4;
caseType = 4;
}
if (bestCase < original - 1e-12) {
double delta = bestCase - original;
switch (caseType) {
case 1:
std::reverse(tour.begin() + i + 1, tour.begin() + j + 1);
break;
case 2:
std::reverse(tour.begin() + j + 1, tour.begin() + k + 1);
break;
case 3:
std::rotate(tour.begin() + i + 1, tour.begin() + j + 1,
tour.begin() + k + 1);
break;
case 4:
std::reverse(tour.begin() + j + 1, tour.begin() + k + 1);
std::rotate(tour.begin() + i + 1, tour.begin() + j + 1,
tour.begin() + k + 1);
break;
}
currentLen += delta;
improved = true;
}
}
}
}
return improved;
}
d、开放路径自动起终点------「最长边断开法」
- 先不管开放,按闭合 TSP 跑一遍 2/3-opt,得到闭合最优;
- 在闭合路径里挑出最长的那一段边;
- 在此边之后打断,顺时针输出,就得到了开放 TSP 的近似最优解。
cpp
int TSPAlgo::findBestOpenBreak(const QVector<Point>& points,
const QVector<int>& closedTour) {
int n = closedTour.size();
double worstEdgeLen = -1.0;
int worstEdgeIdx = 0;
for (int i = 0; i < n; ++i) {
int a = closedTour[i];
int b = closedTour[(i + 1) % n];
double d = points[a].distTo(points[b]);
if (d > worstEdgeLen) { worstEdgeLen = d; worstEdgeIdx = i; }
}
return worstEdgeIdx;
}
然后把闭环从 breakEdgeIdx + 1 处旋转成起点:
cpp
QVector<int> TSPAlgo::breakTourAt(const QVector<int>& closedTour,
int breakEdgeIdx, int makeStartIdx) {
int n = closedTour.size();
if (n <= 1) return closedTour;
breakEdgeIdx = ((breakEdgeIdx % n) + n) % n;
QVector<int> open = closedTour;
int startPos = (breakEdgeIdx + 1) % n;
if (startPos > 0)
std::rotate(open.begin(), open.begin() + startPos, open.end());
if (makeStartIdx >= 0) {
int pos = open.indexOf(makeStartIdx);
if (pos > 0) {
if (pos > n / 2) std::reverse(open.begin(), open.end());
pos = open.indexOf(makeStartIdx);
if (pos > 0) std::rotate(open.begin(), open.begin() + pos, open.end());
}
}
return open;
}
e、算法实现总入口
cpp
QVector<int> TSPAlgo::solve(const QVector<Point>& points, TSPPathMode mode,
int fixedStartIdx, int maxIterations) {
int n = points.size();
if (n <= 1) {
QVector<int> r;
for (int i = 0; i < n; ++i) r.push_back(i);
return r;
}
if (n == 2) {
if (fixedStartIdx == 1) return {1, 0};
return {0, 1};
}
// 闭合路径
if (mode == TSPPathMode::ClosedLoop) {
QVector<int> tour = nearestNeighbor(points);
tour = twoOpt(points, tour, true, maxIterations);
if (n <= 300) {
tour = threeOpt(points, tour, true, qMax(1, maxIterations / 3));
}
int pos = -1;
if (fixedStartIdx >= 0 && fixedStartIdx < n) {
pos = tour.indexOf(fixedStartIdx);
} else {
int brk = findBestOpenBreak(points, tour);
pos = (brk + 1) % n;
}
if (pos > 0) std::rotate(tour.begin(), tour.begin() + pos, tour.end());
return tour;
}
// 开发路径指定起点
if (fixedStartIdx >= 0 && fixedStartIdx < n) {
QVector<int> tour = nearestNeighbor(points, fixedStartIdx);
tour = twoOpt(points, tour, false, maxIterations);
if (n <= 200) {
tour = threeOpt(points, tour, false, qMax(1, maxIterations / 3));
}
if (!tour.isEmpty() && tour.first() != fixedStartIdx &&
tour.last() == fixedStartIdx) {
std::reverse(tour.begin(), tour.end());
}
return tour;
}
// 开发路径自动起点:闭合优化 → 最长边断开 → 再轻量 open 2-opt(open 3-opt)
QVector<int> closed = nearestNeighbor(points);
closed = twoOpt(points, closed, true, maxIterations);
if (n <= 300) {
closed = threeOpt(points, closed, true, qMax(1, maxIterations / 3));
}
int brk = findBestOpenBreak(points, closed);
QVector<int> openTour = breakTourAt(closed, brk, -1);
openTour = twoOpt(points, openTour, false, maxIterations / 2);
if (n <= 200) {
openTour = threeOpt(points, openTour, false, qMax(1, maxIterations / 5));
}
return openTour;
}
至于可视化部分就不详细讲解了
对你有用就点个赞👍,以后需要用到就收藏⭐