每日算法 — 使用java实现旅行商问题:状态压缩动态规划与最短回路搜索

旅行商问题(Traveling Salesman Problem,TSP)是组合优化领域最具代表性的NP-hard问题之一。问题描述极为直观:一位商人需要拜访若干城市,每对城市之间有已知距离,要求找到一条从起点出发、经过每个城市恰好一次并最终回到起点的最短回路。本文用Java实现基于状态压缩动态规划的精确解法,深入讲解位运算状态表示、子集递推与路径重建的完整流程。

问题建模与复杂度分析

问题定义

给定n个城市组成的完全图,城市编号为0n-1dist[i][j]表示城市i到城市j的距离。目标是找到一条哈密顿回路(经过每个顶点恰好一次的环),使得总距离最小。

为什么暴力不可行

n个城市的排列共有(n-1)!/2种(固定起点并消除方向对称)。当n=20时,排列数约为6×10^16,穷举完全不可行。因此需要更聪明的算法——动态规划。

动态规划的核心思想

TSP具有最优子结构:假设最短回路中从城市0出发,第一步走到城市k,那么从k出发走完剩余所有城市并返回0的子路径,也必须是最短的。基于此,定义状态:

dp[mask][i] = 从城市0出发,已经访问过的城市集合为mask,当前位于城市i的最短距离

其中mask是一个n位二进制数,第j位为1表示城市j已被访问。

状态压缩的位运算技巧

状态表示

  • mask的范围是0(1<<n)-1,共2^n种状态
  • 检查城市i是否被访问:if ((mask & (1 << i)) != 0)
  • 将城市i标记为已访问:mask | (1 << i)
  • 将城市i从集合中移除:mask ^ (1 << i)mask & ~(1 << i)

状态转移方程

dp[mask][i] = min(dp[mask ^ (1 << i)][j] + dist[j][i])  对所有 j∈mask 且 j≠i 且 j≠0

含义:要到达状态(mask, i),上一步必然在某个城市j,从j走到i。去掉i后得到子状态(mask^(1<<i), j)

初始化与边界

  • dp[1 << 0][0] = 0:只访问了城市0,当前位于城市0,距离为0
  • 其余所有状态初始化为正无穷

最终答案

answer = min(dp[(1<<n)-1][i] + dist[i][0])  对所有 i≠0

所有城市都已访问(mask全为1),从最后一个城市i返回起点0。

完整Java实现

以下代码实现了TSP的状态压缩DP解法,包含核心算法、路径重建与复杂度分析。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 旅行商问题(TSP)—— 状态压缩动态规划精确解法
 * 时间复杂度: O(n^2 * 2^n)
 * 空间复杂度: O(n * 2^n)
 * 适用规模: n <= 20(状态数 2^20 ≈ 100万)
 */
public class TravelingSalesmanProblem {

    // 无穷大表示不可达或初始状态
    private static final int INF = Integer.MAX_VALUE / 2;

    private final int n;           // 城市数量
    private final int[][] dist;    // 距离矩阵
    private int[][] dp;            // dp[mask][i]: 已访问mask集合,当前在i的最短距离
    private int[][] parent;        // 路径重建:记录dp[mask][i]是从哪个城市转移而来

    public TravelingSalesmanProblem(int[][] dist) {
        this.n = dist.length;
        this.dist = new int[n][n];
        for (int i = 0; i < n; i++) {
            this.dist[i] = Arrays.copyOf(dist[i], n);
        }
        // dp数组大小: 2^n * n
        int totalStates = 1 << n;
        this.dp = new int[totalStates][n];
        this.parent = new int[totalStates][n];
        for (int[] row : dp) {
            Arrays.fill(row, INF);
        }
        for (int[] row : parent) {
            Arrays.fill(row, -1);
        }
    }

    /**
     * 执行状态压缩动态规划求解TSP
     * @return 最短回路的总距离
     */
    public int solve() {
        // 初始状态:只访问了城市0,当前位于城市0
        dp[1 << 0][0] = 0;

        // 枚举所有状态mask
        for (int mask = 1; mask < (1 << n); mask++) {
            // 如果当前状态没有访问城市0,跳过(起点必须是0)
            if ((mask & 1) == 0) continue;

            // 枚举当前所在城市i
            for (int i = 0; i < n; i++) {
                // 如果当前状态mask中没有城市i,跳过
                if ((mask & (1 << i)) == 0) continue;
                // 如果dp[mask][i]不可达,跳过
                if (dp[mask][i] == INF) continue;

                // 尝试从城市i前往下一个未访问的城市j
                for (int j = 0; j < n; j++) {
                    // j必须是未访问的城市,且不能是起点0(最后才回起点)
                    if ((mask & (1 << j)) != 0) continue;

                    int nextMask = mask | (1 << j);
                    int newDist = dp[mask][i] + dist[i][j];
                    if (newDist < dp[nextMask][j]) {
                        dp[nextMask][j] = newDist;
                        parent[nextMask][j] = i; // 记录从i转移到j
                    }
                }
            }
        }

        // 所有城市都已访问,从最后一个城市返回起点0
        int fullMask = (1 << n) - 1;
        int minTour = INF;
        int lastCity = -1;
        for (int i = 1; i < n; i++) {
            if (dp[fullMask][i] == INF) continue;
            int tourLength = dp[fullMask][i] + dist[i][0];
            if (tourLength < minTour) {
                minTour = tourLength;
                lastCity = i;
            }
        }

        return minTour;
    }

    /**
     * 重建最短回路路径
     * @return 城市访问顺序列表(包含起点作为首尾)
     */
    public List<Integer> reconstructPath() {
        int fullMask = (1 << n) - 1;
        int minTour = INF;
        int lastCity = -1;

        // 找到最终状态中的最优终点
        for (int i = 1; i < n; i++) {
            if (dp[fullMask][i] == INF) continue;
            int tourLength = dp[fullMask][i] + dist[i][0];
            if (tourLength < minTour) {
                minTour = tourLength;
                lastCity = i;
            }
        }

        if (lastCity == -1) {
            return Collections.emptyList();
        }

        // 逆序回溯路径
        List<Integer> path = new ArrayList<>();
        int mask = fullMask;
        int cur = lastCity;
        while (cur != -1) {
            path.add(cur);
            int prev = parent[mask][cur];
            mask ^= (1 << cur);
            cur = prev;
        }
        Collections.reverse(path);
        path.add(0); // 回到起点,形成回路
        return path;
    }

    /**
     * 打印dp状态表(仅用于小规模调试)
     */
    public void printDPState() {
        System.out.println("=== DP状态表 ===");
        for (int mask = 0; mask < (1 << n); mask++) {
            System.out.printf("mask=%d: ", mask);
            for (int i = 0; i < n; i++) {
                if (dp[mask][i] == INF) {
                    System.out.print("INF ");
                } else {
                    System.out.print(dp[mask][i] + " ");
                }
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        // 示例:4个城市及其之间的距离矩阵
        // 城市0=A, 1=B, 2=C, 3=D
        int[][] dist = {
            {0, 10, 15, 20},
            {10, 0, 35, 25},
            {15, 35, 0, 30},
            {20, 25, 30, 0}
        };

        TravelingSalesmanProblem tsp = new TravelingSalesmanProblem(dist);
        int minLength = tsp.solve();

        System.out.println("=== 旅行商问题(TSP)状态压缩DP求解 ===\n");
        System.out.println("最短回路总距离: " + minLength);

        List<Integer> path = tsp.reconstructPath();
        System.out.print("最优访问顺序: ");
        for (int i = 0; i < path.size(); i++) {
            char city = (char) ('A' + path.get(i));
            System.out.print(city);
            if (i < path.size() - 1) {
                System.out.print(" -> ");
            }
        }
        System.out.println();

        // 更大规模测试(8个城市)
        System.out.println("\n=== 8城市规模测试 ===");
        int[][] dist8 = {
            {0, 29, 82, 46, 68, 52, 72, 42},
            {29, 0, 55, 46, 42, 43, 43, 23},
            {82, 55, 0, 68, 46, 55, 23, 43},
            {46, 46, 68, 0, 82, 15, 72, 31},
            {68, 42, 46, 82, 0, 74, 23, 52},
            {52, 43, 55, 15, 74, 0, 61, 23},
            {72, 43, 23, 72, 23, 61, 0, 42},
            {42, 23, 43, 31, 52, 23, 42, 0}
        };

        TravelingSalesmanProblem tsp8 = new TravelingSalesmanProblem(dist8);
        long start = System.currentTimeMillis();
        int min8 = tsp8.solve();
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("8城市最短距离: " + min8);
        System.out.println("计算耗时: " + elapsed + "ms");
        System.out.print("最优路径: ");
        for (int city : tsp8.reconstructPath()) {
            System.out.print(city + " ");
        }
        System.out.println();
    }
}

路径重建原理

路径重建依赖parent[mask][i]数组。在状态转移时,一旦dp[nextMask][j]被更新,就记录parent[nextMask][j] = i,表示到达状态(nextMask, j)的最优路径中,上一个城市是i

重建时从最终状态(fullMask, lastCity)出发,不断查询parent并移除当前城市对应的位,逆序收集所有城市后反转即可。最后将起点0追加到末尾,形成完整回路。

复杂度分析

指标 说明
时间复杂度 O(n² × 2ⁿ) 共2ⁿ个状态,每个状态转移需O(n)
空间复杂度 O(n × 2ⁿ) dp与parent两个二维数组
适用规模 n ≤ 20 2²⁰ ≈ 100万状态,可秒级求解
精确性 全局最优 与穷举等价,保证找到最短回路

n > 20时,状态数呈指数爆炸,此时应转向启发式算法(如模拟退火、遗传算法、蚁群算法)或近似算法(如最近邻、最小生成树近似)。

扩展:非对称TSP

上述实现天然支持非对称距离dist[i][j] ≠ dist[j][i]),只需在输入时提供任意距离矩阵即可。若某些城市之间不可达,可将对应距离设为INF,算法会自动排除这些边。

总结

旅行商问题是理解NP-hard问题与动态规划结合的绝佳案例。状态压缩技巧将指数级枚举转化为”状态×当前位置”的递推结构,利用位运算高效表示集合,使得中等规模(n≤20)的TSP可以在可接受时间内精确求解。本文实现的Java版本完整覆盖了算法核心、路径重建与性能测试,可直接集成到路径优化、物流调度等实际项目中。