状态压缩DP---最短Hamilton路径

给定一张 nn 个点的带权无向图,点从 0∼n−10∼n−1 标号,求起点 00 到终点 n−1n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 00 到 n−1n−1 不重不漏地经过每个点恰好一次。

输入格式

第一行输入整数 nn。

接下来 nn 行每行 nn 个整数,其中第 ii 行第 jj 个整数表示点 ii 到 jj 的距离(记为 a[i,j]a[i,j])。

对于任意的 x,y,z 数据保证 a[x,x]=0,a[x,y]=a[y,x]a  并且 a[x,y]+a[y,z]≥a[x,z] 。

输出格式

输出一个整数,表示最短 Hamilton 路径的长度。

数据范围

1≤n≤20
0≤a[i,j]≤10e7

输入样例:

解释

5 0 2 4 5 1 2 0 6 5 3 4 6 0 8 3 5 5 8 0 5 1 3 3 5 0
输出样例:
18

这道题是经典的 旅行商问题(TSP) 的变种,它要求求解最短的 Hamilton 路径,即从起点 0 到终点 n-1,恰好经过每个点一次。题目的难点在于如何使用状态压缩和动态规划来高效地求解这个问题。

问题解析

  • 状态压缩:我们用一个整数的二进制表示来记录 Hamilton 路径的状态,每一位代表一个顶点是否被访问过。比如,对于一个包含 n=5 个点的图,状态 01101 表示顶点 0、2 和 3 已经被访问过,而其他顶点尚未访问。

  • 动态规划:用 f[state][j] 表示当前的状态是 state,且最后一个访问的点是 j 的最短路径长度。state 是一个长度为 n 的二进制数,表示经过的点集合,j 是最后访问的那个点。

状态转移方程

  • 对于每一个状态 state,我们要从中找到可能的倒数第二步 k,然后通过 k -> j 进行状态转移。

  • 状态转移方程为:

    f[state][j] = min(f[state][j], f[state - (1 << j)][k] + w[k][j])

    其中 state - (1 << j) 表示去掉当前最后一个访问点 j 后的状态,通过找到从点 k 到点 j 的路径更新 f[state][j]

初始状态

  • 起点是顶点 0,因此我们初始化 f[1][0] = 0,表示从顶点 0 开始,路径长度为 0,其他点的路径长度初始化为无穷大。

状态枚举

  • 我们枚举所有可能的状态 state,每个状态都表示经过了一些顶点。

  • 对于每个状态 state,我们进一步枚举最后一个访问的顶点 j,然后再通过倒数第二个访问的顶点 k 来更新 f[state][j] 的值。

最终答案

  • 最终的结果是 f[(1 << n) - 1][n - 1],表示从起点 0 出发,经过所有顶点,最终到达终点 n-1 的最短路径长度。

状态压缩与解题思路

  1. 状态表示:用二进制数表示已经访问过的顶点集合。状态共有 2^n 种可能,其中 n 是顶点数,每一位代表一个顶点是否被访问过。

  2. 状态转移:对于每个状态 state,我们通过倒数第二步 k 来更新路径长度,并确保每个顶点恰好访问一次。

  3. 动态规划:通过记录每个状态下的最短路径长度,不断更新最优解,直到遍历所有可能的状态和路径。

代码分析

import java.util.*;
public class Main{
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int N = 20, M = 1 << N;
        int[][] f = new int[M][N]; // f[state][j]: state状态下最后到达点j的最短路径
        int[][] w = new int[N][N]; // 权重矩阵 w[i][j]: 点i到点j的距离
        int n = scan.nextInt(); // 点的个数
        
        // 读取图的邻接矩阵
        for(int i = 0 ; i < n ; i ++ )
            for(int j = 0 ; j < n ; j ++ )
                w[i][j] = scan.nextInt(); 
        
        // 初始化动态规划数组,设置为正无穷
        for(int i = 0 ; i < 1 << n ; i ++ )
            Arrays.fill(f[i],0x3f3f3f); 
        f[1][0] = 0; // 起点是顶点0,状态为只访问了顶点0
        
        // 枚举所有的状态
        for(int state = 0 ; state < 1 << n ; state ++ ){
            for(int j = 0 ; j < n ; j ++ ){
                // 判断当前状态下是否访问过顶点 j
                if((state >> j & 1) == 1){ 
                    for(int k = 0 ; k < n ; k ++ ){
                        // 判断倒数第二步是否访问过顶点 k
                        if((state - (1 << j) >> k & 1) == 1){
                            // 状态转移
                            f[state][j] = Math.min(f[state][j], f[state - (1 << j)][k] + w[k][j]);
                        }
                    }
                }
            }
        }
        
        // 输出最终结果,表示从顶点0开始访问所有顶点到终点n-1的最短路径
        System.out.println(f[(1 << n) - 1][n - 1]);
    }
}

重点总结

  • 状态压缩:通过二进制位表示顶点访问的状态,状态总数为 2^n

  • 动态规划:利用 f[state][j] 表示当前状态为 state 并且最后访问点为 j 的最短路径长度,通过枚举状态和顶点进行状态转移。

  • 最终输出:通过 f[(1 << n) - 1][n - 1] 获取所有顶点访问完后的最短路径。

这个问题的难点在于如何使用状态压缩高效表示和转移状态。通过动态规划和状态压缩,我们可以在指数时间内解决这个 Hamilton 路径问题。


我们来详细分析一下在这道题目中如何对 state 进行操作。

什么是 state

state 是一个二进制数,每一位(bit)表示一个顶点是否被访问过。例如:

  • 如果有 4 个顶点:n = 4

  • 那么 state 是一个 4 位的二进制数,每一位表示对应的顶点是否已经访问过:

    • state = 0011 表示顶点 0 和 1 已经被访问过,而顶点 2 和 3 尚未访问。

这样,我们可以通过二进制的某一位来记录顶点的访问情况,并且可以通过位运算对状态进行操作。

操作 state 的位运算解释

  1. 检查一个顶点是否已经被访问过

    • 通过位运算 (state >> j) & 1 来判断 state 的第 j 位是否为 1。如果结果为 1,说明顶点 j 已经被访问过;如果结果为 0,说明顶点 j 尚未访问。

    • 举个例子:

      • state = 0110,我们想检查顶点 2 是否已经访问过。

      • (state >> 2) & 1 = (0110 >> 2) & 1 = 0001 & 1 = 1,所以顶点 2 已经访问过。

  2. 将顶点 jstate 中移除

    • 使用 state - (1 << j) 表示将顶点 j 从访问的顶点集合中移除。

    • 1 << j 表示一个二进制数,只有第 j 位是 1,其他位是 0。通过 state - (1 << j),我们相当于将 state 的第 j 位从 1 变成 0,表示移除了顶点 j

    • 例如:

      • 如果 state = 0111,表示顶点 0、1、2 已经访问。我们想要移除顶点 2:

      • state - (1 << 2) = 0111 - 0100 = 0011,结果是 0011,即只剩下顶点 0 和 1 被访问。

  3. 状态转移公式解释

    • 公式:

      f[state][j] = Math.min(f[state][j], f[state - (1 << j)][k] + w[k][j]);
    • 解释:

      • f[state][j] 表示:当前的状态是 state,最后访问的顶点是 j 的最短路径长度。

      • f[state - (1 << j)][k] + w[k][j] 表示:从状态 state - (1 << j) 转移到状态 state,最后一步从顶点 k 到顶点 j,需要加上边 k -> j 的权重。

      • 我们通过遍历所有可能的倒数第二个顶点 k,来计算从 k -> j 的最短路径。

  4. 状态转移的过程

    for (int state = 0; state < 1 << n; state++) 这段代码中,我们枚举了所有的状态,也就是所有可能的顶点访问组合。对于每个状态,我们遍历每个可能的顶点 j(这个顶点是当前状态下最后访问的顶点),然后再枚举倒数第二步的顶点 k,计算从 kj 的路径长度并更新最短路径。

    • 状态 state:表示哪些顶点已经访问过。

    • 最后访问的顶点 j:我们要确定当前状态下的最后一个访问的顶点 j

    • 倒数第二步的顶点 k:我们通过 state - (1 << j) 表示在不包括顶点 j 的情况下哪些顶点已经访问过,然后找到倒数第二个访问的顶点 k

  5. 最终结果

    最终我们输出 f[(1 << n) - 1][n - 1]。这里 (1 << n) - 1 是一个状态,其中所有顶点都被访问过,最后一个访问的顶点是 n-1,表示起点 0 经过所有顶点,最后到达终点 n-1 的最短路径。

总结

  • state 的操作:我们使用二进制位来表示顶点是否被访问过,并通过位运算来进行状态的查询、更新和转移。

  • 状态转移:通过动态规划记录从一个状态到另一个状态的最短路径,最终计算出从起点 0 到终点 n-1,经过每个顶点恰好一次的最短路径长度。

通过这些操作,状态压缩大大减少了对状态的存储需求,并通过位运算高效地进行状态转移。

 

你可能感兴趣的:(力扣,算法,数据结构)