【三维TSP可视化】1000个点以内的最优路径:最近邻 + 2-opt + 3-opt 组合算法实战(Qt实现)

一、目的

旅行商问题(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、开放路径自动起终点------「最长边断开法」
  1. 先不管开放,按闭合 TSP 跑一遍 2/3-opt,得到闭合最优;
  2. 在闭合路径里挑出最长的那一段边
  3. 在此边之后打断,顺时针输出,就得到了开放 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;
}

至于可视化部分就不详细讲解了

项目源码

对你有用就点个赞👍,以后需要用到就收藏⭐

相关推荐
qeen874 小时前
【数据结构】自平衡二叉搜索树各种旋转算法原理解析及AVL树的C++实现
数据结构·c++·算法
lucas_AI4 小时前
1.2B 小模型赢过 235B 大模型:NaviDC-OCR 把文档解析卷明白了
人工智能·深度学习·算法
冻柠檬飞冰走茶5 小时前
PTA基础编程题目集 7-35有理数均值(C++语言实现)
开发语言·数据结构·c++·算法·均值算法
民乐团扒谱机5 小时前
【微实验】倒谱算法(Cepstrum)深度解析:原理、数学推导与代码实现
人工智能·算法·语音识别
Nil2086 小时前
leetcode 189轮转数组
数据结构·算法·leetcode
-dzk-6 小时前
【动态规划】LC 118.杨辉三角
算法·动态规划
恣逍信点6 小时前
《凌微经》静态自悖——变化之自因
人工智能·科技·算法·机器学习·业界资讯·拓扑学·哲学
吃着火锅x唱着歌7 小时前
LeetCode 3885.设计事件管理器
算法·leetcode·职场和发展
土司大王7 小时前
LeetCode hto100——字母异位词分组
java·算法·leetcode
Eloudy7 小时前
预词力:LLM 的唯一形式化能力
人工智能·算法·agent