1. 什么是旅行售货员问题?
旅行售货员问题(Traveling Salesman Problem,简称 TSP),也叫旅行商问题,是组合优化和计算机科学中最著名的难题之一。
问题的描述很简单:
有一个售货员要拜访 个城市,每个城市恰好拜访一次,最后回到出发城市。已知任意两个城市之间的距离,求总路程最短的环游路线。
例如, 个城市 A、B、C、D,售货员从 A 出发,一条合法路线是:
它经过了所有城市各一次,并回到了起点。
NOTETSP 表面上像一个”排路线”的小问题,但它是经典的 NP-Hard 问题。目前没有任何多项式时间的精确算法,规模一大就只能靠剪枝、近似或启发式方法来求解。
2. 问题的数学描述
把城市看作图的顶点,城市之间的道路看作带权边,TSP 就是:在一个 带权完全图 上,找一条权重最小的哈密顿回路(Hamiltonian cycle)。
设 个城市编号为 ,距离矩阵为 ,表示城市 到城市 的距离。一条环游可以表示为一个排列:
其中 是出发城市, 是其余城市的访问顺序。总路程为:
第一项是从出发城市一路走到最后一个城市的距离,第二项是从最后一个城市回到起点的距离。目标是:
NOTE距离矩阵不一定是对称的(去程和回程距离可以不同),也不一定满足三角不等式。本文为方便演示,使用对称的距离矩阵。
3. 解空间:排列树
由于售货员要给除起点外的每个城市排定一个访问顺序,所以 TSP 的解空间就是排列。
固定出发城市为 (因为环游从哪个城市出发都是等价的),剩下的 个城市做全排列。因此解空间是一棵排列树,这和 素数环问题 的结构是一样的。
不同的环游方案数上界为:
为什么是 而不是因为环游是一个回路,起点选哪个城市不影响结果,所以固定一个出发城市,只排列剩下的 个城市即可。如果距离对称,正向和反向是同一条回路,还可以再除以 ,即 。
排列树长这样(以 为例):
起点 0 / | \ 选 1 选 2 选 3 / \ ... ... 选 2 选 3 | | 选 3 选 2 | | 回到 0 回到 0回溯法就是在这棵排列树上做深度优先遍历:能走就走,走不通或明显不优就回头。
4. 回溯法
4.1 基本思路
回溯法的核心步骤:
- 固定出发城市为 ,标记已访问。
- 从第 个位置开始,依次尝试每个未访问的城市。
- 把选中城市加入路径,累加距离,递归进入下一层。
- 当所有城市都访问完,加上回到起点的距离,得到一条完整环游的总路程,更新最优解。
- 递归返回后,撤销选择(回溯),尝试下一个城市。
回溯核心口诀做出选择 递归探索下一层 撤销选择(恢复现场)。
4.2 关键剪枝:代价超过当前最优即放弃
纯排列树遍历是 ,规模稍大就爆炸。最直接的剪枝是:
如果当前已经走过的距离
currentCost加上下一步的距离,已经大于等于已知的最优解bestCost,那么这条路再走下去只会更差,直接剪掉。
这就是分支限界思想的雏形:用一个界来判断某个分支是否还有希望。
5. 以 4 个城市为例走一遍
设 个城市 A、B、C、D(编号 ),距离矩阵为:
A B C DA ∞ 10 15 20B 10 ∞ 35 25C 15 35 ∞ 30D 20 25 30 ∞从 A(城市 )出发,列出所有合法环游(共 条,对称距离下成对相等):
A→B→C→D→A:10 + 35 + 30 + 20 = 95A→B→D→C→A:10 + 25 + 30 + 15 = 80 ← 最优A→C→B→D→A:15 + 35 + 25 + 20 = 95A→C→D→B→A:15 + 30 + 25 + 10 = 80 ← 最优(上一条的逆向)A→D→B→C→A:20 + 25 + 35 + 15 = 95A→D→C→B→A:20 + 30 + 35 + 10 = 95最优总路程是 ,对应路线 。
下面按代码中的 DFS 顺序演示搜索过程。bestCost 初始为 。
第 1 步:固定起点 A,path = [A]。
第 2 步:选 B。
A → B currentCost = 10第 3 步:选 C。
A → B → C currentCost = 10 + 35 = 45第 4 步:只剩 D,选 D,回到 A。
A → B → C → D → A 总路程 = 45 + 30 + 20 = 95得到一个解,bestCost = 95。回溯。
回到第 3 步:换成选 D。
A → B → D → C → A 总路程 = 10 + 25 + 30 + 15 = 80,更新 bestCost = 80。回溯。
回到第 2 步:换成选 C。进入后:
A → C currentCost = 15继续尝试剩余城市。
- 选 B:
A → C → B的currentCost = 15 + 35 = 50,再选 D 并回到 A,总路程为50 + 25 + 20 = 95,不更新。 - 选 D:
A → C → D的currentCost = 15 + 30 = 45,再选 B 并回到 A:
A → C → D → B → A 总路程 = 45 + 25 + 10 = 80,没有更优,不更新。
回到第 2 步:最后换成选 D。进入后 A → D 的 currentCost = 20。
- 选 B:
A → D → B的currentCost = 20 + 25 = 45。再尝试 C 时,nextCost = 45 + 35 = 80,已经不小于bestCost = 80,剪枝。 - 选 C:
A → D → C的currentCost = 20 + 30 = 50。再尝试 B 时,nextCost = 50 + 35 = 85,也被剪枝。
搜索结束,最终最优解为 。
NOTE第一次找到的解(这里 )会成为
bestCost。后续一旦某个部分路径的nextCost >= bestCost,就可以提前放弃;当bestCost更新为 后,剪枝会更强。
6. Java 实现(回溯法)
public class TSPBacktracking {
private static int n; private static int[][] dist; // 距离矩阵 private static boolean[] visited; // 标记城市是否已访问 private static int[] path; // 当前路径 private static int[] bestPath; // 最优路径 private static int currentCost; // 当前已走距离 private static int bestCost; // 最优总路程
public static int solve(int[][] matrix) { n = matrix.length; dist = matrix; visited = new boolean[n]; // 第 n 个位置用于存放回到起点的城市 path = new int[n + 1]; bestPath = new int[n + 1]; bestCost = Integer.MAX_VALUE; currentCost = 0;
// 固定从城市 0 出发 visited[0] = true; path[0] = 0;
backtrack(1); return bestCost; }
private static void backtrack(int index) { // 所有城市都已访问,加上回到起点的距离 if (index == n) { int total = currentCost + dist[path[n - 1]][0]; if (total < bestCost) { bestCost = total; path[n] = 0; // 回到起点 System.arraycopy(path, 0, bestPath, 0, n + 1); } return; }
for (int i = 0; i < n; i++) { if (visited[i]) { continue; }
// 剪枝:当前代价 + 这一步 已经不优于最优解,放弃 int nextCost = currentCost + dist[path[index - 1]][i]; if (nextCost >= bestCost) { continue; }
// 做出选择 visited[i] = true; path[index] = i; currentCost = nextCost;
// 递归进入下一层 backtrack(index + 1);
// 撤销选择 currentCost -= dist[path[index - 1]][i]; visited[i] = false; } }
public static void main(String[] args) { // 用 ∞ 表示不存在的边,这里城市 0..3 对应 A..D int INF = Integer.MAX_VALUE >> 1; // 取半是为了防止相加溢出 int[][] matrix = { {INF, 10, 15, 20}, { 10, INF, 35, 25}, { 15, 35, INF, 30}, { 20, 25, 30, INF} };
int result = solve(matrix);
System.out.println("最短路程:" + result); System.out.print("最优路线:"); for (int i = 0; i <= n; i++) { System.out.print((char) ('A' + bestPath[i])); if (i != n) { System.out.print(" -> "); } } System.out.println(); }}运行结果:
最短路程:80最优路线:A -> B -> D -> C -> AINF 取值的注意点代码里用
Integer.MAX_VALUE >> 1表示无穷大,是为了防止两个INF相加时溢出变成负数,从而被误判为”更优”。处理带权图时这是常见的细节坑。
7. 复杂度分析(回溯法)
- 时间复杂度:最坏情况 。
- 空间复杂度:,包括
path、bestPath、visited数组和递归调用栈。
为什么是阶乘?因为排列树在第 层有 个分支,第 层每个节点最多 个分支,依次递减,叶子节点数为:
实际运行中,“代价超过最优即剪枝”会砍掉大量分支,但最坏情况仍是阶乘级。对这类 NP-Hard 问题,精确算法通常需要配合更强的剪枝或状态压缩,才能把可处理规模向前推进。
8. 用下界剪枝:分支限界思想
第 6 节的剪枝只用到了”当前已走距离”,这个界太松——它完全不考虑剩余城市还要花多少距离。分支限界法(Branch and Bound) 的核心是:给每个部分解估算一个更紧的下界,下界越大,越能提前剪枝。
8.1 什么是好的下界?
下界必须满足:
我们要找的是更短路线。
若某个分支的下界已经 ,说明它再往下搜也不可能得到更短结果,可以直接剪掉。
8.2 矩阵归约法(经典下界)
一种经典且较紧的下界来自距离矩阵归约(matrix reduction),思路是:
一条环游从每个城市出发一次,也到达一次。因此从距离矩阵每一行减去该行最小值、每一列减去该列最小值,所减掉的总量,是任何环游都必须至少付出的代价。
先用一个生活例子理解“下界”。
假设你要买 4 样东西,每样都必须买一次。你还不知道最后具体在哪家店买,但已经知道:
第 1 样东西最便宜也要 10 元第 2 样东西最便宜也要 10 元第 3 样东西最便宜也要 15 元第 4 样东西最便宜也要 20 元那你可以先得到一个肯定成立的结论:
总价至少是 10 + 10 + 15 + 20 = 55 元最后真正花的钱可能是 80、95 或 120,但不可能低于 55。这个“至少要花 55 元”就是下界。
矩阵归约法在 TSP 里做的事情类似:虽然还不知道最终路线怎么走,但每个城市至少要选一条离开的边,也至少要选一条进入的边。于是可以先估算:无论最后怎么绕,这条环游至少要花多少钱。
具体步骤:
- 行归约:每一行减去本行最小元素,记录所有行最小值之和 。
- 列归约:对行归约后的矩阵,每一列减去本列最小元素,记录所有列最小值之和 。
- 下界:
为什么这是合法下界?因为环游在每一行恰好选一个元素(从该城市出发的一条边),在每一列也恰好选一个元素(到达该城市的一条边)。每行至少贡献行最小值,每列至少贡献列最小值,所以减掉的总量是任何环游的下界。
8.3 用同一个例子算下界
原始矩阵:
A B C DA ∞ 10 15 20B 10 ∞ 35 25C 15 35 ∞ 30D 20 25 30 ∞第一步:行归约。 各行最小值为 ,和为 。每行减去最小值:
A B C DA ∞ 0 5 10B 0 ∞ 25 15C 0 20 ∞ 15D 0 5 10 ∞第二步:列归约。 各列最小值为 ,需要减的只有 C 列(减 )和 D 列(减 ),和为 。归约后:
A B C DA ∞ 0 0 0B 0 ∞ 20 5C 0 20 ∞ 5D 0 0 5 ∞下界:
而本例最优解是 ,,下界成立。
下界越紧,剪枝越强朴素剪枝只看当前已走距离
currentCost,相当于把“剩下还要走的路”估成 。这个估计一定安全,但太乐观。矩阵归约会给剩余部分也估一个“至少还要花多少”的下界。把它加到
currentCost上,就能得到更接近真实总路程的判断:如果这个值已经不小于
bestCost,说明这条分支就算后面每一步都走得很理想,也不可能超过当前最优解,可以提前剪掉。
8.4 在搜索中维护下界
实际搜索时,每个节点都要回答一个问题:
我已经走到这里了,如果把剩下的路用最乐观的方式估一下,这条分支还有机会超过当前最优解吗?
例如现在已经找到一条完整路线,bestCost = 80。搜索到某个部分路径时:
A → DcurrentCost = 20如果用矩阵归约估出“剩下的城市至少还要花 65”,那么这条分支的乐观总成本就是:
20 + 65 = 85由于 ,即使后面每一步都按最理想情况走,也不可能得到更短路线,所以这整个分支可以直接剪掉。
维护这个下界时,每选定一条边(比如确定 ),就把对应行和列从矩阵中划掉,并对剩余子矩阵重新归约,更新下界增量。这样每个搜索节点都有一个反映“已确定路径 + 剩余最乐观估计”的下界。
完整的优先队列式分支限界会把这些搜索节点放进堆里,每次优先扩展下界最小、最有希望的节点。它效果更好,但实现也更复杂。本文的回溯代码已经展示了“界 + 剪枝”的核心思想:把简单的 currentCost 剪枝换成“当前成本 + 剩余下界”剪枝,就进入了分支限界法的思路。
9. 其他常见方法概览
TSP 是个研究了几十年的问题,精确法和近似/启发式方法都很多。
| 方法 | 类型 | 时间复杂度 | 特点 |
|---|---|---|---|
| 回溯法 | 精确 | 实现简单,适合小规模 | |
| 分支限界法 | 精确 | 最坏仍指数级 | 下界紧,剪枝强,常用于中等规模 |
| 动态规划(Held-Karp) | 精确 | 用状态压缩, 左右可行 | |
| 最近邻等贪心法 | 近似 | 快但不保证最优,可作为初始解 | |
| 模拟退火 / 遗传算法 / 蚁群 | 启发式 | 依赖迭代次数 | 大规模近似求解,结果不稳定 |
Held-Karp 动态规划用 表示已访问城市集合为 、当前位于城市 的最短距离。状态数为 ,转移 ,总复杂度 。它把阶乘级降到了指数级,是精确解法里最快的之一,但 一旦超过 也很难承受。
选择哪种方法,取决于问题规模和精度要求:
- :回溯法足矣。
- :动态规划 Held-Karp。
- 左右:分支限界(下界要设计得好)。
- 更大:近似算法或元启发式算法。
10. 总结
旅行售货员问题的核心可以概括为:
在排列树上搜索所有环游,用下界剪掉无望分支,找到总路程最短的回路。
核心内容如下:
- TSP 的解空间是排列树,固定起点后方案数上界为 。
- 回溯法通过”做出选择 → 递归 → 撤销选择”遍历排列树。
- 最朴素的剪枝:当前已走距离 已知最优,即剪掉。
- 分支限界法用更紧的下界(如矩阵归约)替代朴素界,剪枝更强。
- TSP 是 NP-Hard 问题,精确算法都是指数级以上,大规模只能靠近似。
口诀:
固定起点排顺序,回溯遍历排列树;代价超界即剪枝,归约下界更强力。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时


















