java---状态压缩dp---最短Hamilton路径(每日一道算法2022.10.23)

题目:
给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径
Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次

第一行输入整数 n。
接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])
对于任意的 x,y,z,数据保证 a[x,x] = 0,a[x,y] = a[y,x] 并且 a[x,y] + a[y,z] ≥ a[x,z]

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

1 ≤ n ≤ 20
0 ≤ a[i,j] ≤ 107

输入:
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
public class 状态压缩dp_最短Hamilton路径 {
    //w存邻接矩阵,f存状态,f[state][j]表示从0到j点,点集为state的所有路径
    public static int N = 20, M = 1 << N,INF = 0x3f3f3f, n;
    public static int[][] w = new int[N][N];
    public static int[][] f = new int[M][N];

    public static void main(String[] args) {
        //读入邻接矩阵
        Scanner in = new Scanner(System.in);
        n = in.nextInt();
        for (int i = 0; i<n; i++) {
            for (int j = 0; j<n; j++) w[i][j] = in.nextInt();
        }

        //将所有状态初始化为正无穷,然后base case是从0点走到0点,state为1,表示走过了第0点,最短路径为0
        for (int i = 0; i < (1<<n); i++) Arrays.fill(f[i], INF);
        f[1][0] = 0;

        //状态计算
        for (int state = 0; state < (1<<n); state++) {  //枚举所有状态state(二进制点集,比如1101代表走过第0,2,3点)
            for (int j = 0; j<n; j++) {                 //枚举所有点j
                if ((state >> j & 1) == 1) {            //只有当state中包含j点时(也就是经过过j点),再进行状态转移
                    for (int k = 0; k<n; k++) {         //枚举走到j点前, 以k为终点的最短距离
                        if ((state >> k & 1) == 1) {    //只有当state中同时包含j点和k点(j点刚刚判断过了,现在判断k点),才能用0到k点的最短距离对0到j点的最短距离进行更新
                            f[state][j] = Math.min(f[state][j], f[state ^ (1 << j)][k] + w[k][j]);
                        }
                    }
                }
            }
        }

        //最后输出结果,也就是当所有点都被走到时(也就是state的二进制全部为1时),0到n-1的最短路径
        System.out.println(f[(1 << n) - 1][n-1]);
    }
}

解析(完全照搬垫底抽风大佬的题解:最短Hamilton路径):
抽风大佬这题写的太清晰了,我尝试写了下,效果一般,还是借用下吧,别误人子弟

首先想下暴力算法,这里直接给出一个例子
比如数据有 5 个点,分别是 0, 1, 2, 3, 4
那么在爆搜的时候,会枚举一下六种路径情况(只算对答案有贡献的情况的话):

c a s e   1 :   0 → 1 → 2 → 3 → 4 case\ 1:\ 0 \rightarrow 1 \rightarrow 2 \rightarrow 3 \rightarrow 4 case 1: 01234
c a s e   2 :   0 → 1 → 3 → 2 → 4 case\ 2:\ 0 \rightarrow 1 \rightarrow 3 \rightarrow 2 \rightarrow 4 case 2: 01324
c a s e   3 :   0 → 2 → 1 → 3 → 4 case\ 3:\ 0 \rightarrow 2 \rightarrow 1 \rightarrow 3 \rightarrow 4 case 3: 02134
c a s e   4 :   0 → 2 → 3 → 1 → 4 case\ 4:\ 0 \rightarrow 2 \rightarrow 3 \rightarrow 1 \rightarrow 4 case 4: 02314
c a s e   5 :   0 → 3 → 1 → 2 → 4 case\ 5:\ 0 \rightarrow 3 \rightarrow 1 \rightarrow 2 \rightarrow 4 case 5: 03124
c a s e   6 :   0 → 3 → 2 → 1 → 4 case\ 6:\ 0 \rightarrow 3 \rightarrow 2 \rightarrow 1 \rightarrow 4 case 6: 03214

那么观察一下 c a s e   1 case\ 1 case 1 c a s e   3 case\ 3 case 3,可以发现,我们在计算从点 0 0 0 到点 3 3 3 的路径时,其实并不关心这两中路径经过的点的顺序,而是只需要这两种路径中的较小值,因为只有较小值可能对答案有贡献。

所以,我们在枚举路径的时候,只需要记录两个属性:当前经过的点集,当前到了哪个点
而当前经过的点集不是一个数。观察到数据中点数不会超过 20 20 20,我们可以用一个二进制数表示当前经过的点集。其中第 i i i 位为 1/0 表示是/否经过了点 i i i

然后用闫式 dp 分析法考虑 dp
状态表示: f [ s t a t e ] [ j ] f[state][j] f[state][j]。其中 s t a t e state state 是一个二进制数,表示点集的方法如上述所示

  • 集合:经过的点集为 s t a t e state state,且当前到了点 j j j 上的所有路径
  • 属性:路径总长度的最小值

状态计算:假设当前要从点 k k k 转移到 j j j。那么根据 H a m i l t o n Hamilton Hamilton 路径的定义,走到点 k k k 的路径就不能经过点 j j j,所以就可以推出状态转移方程f[state][j] = min{f[state ^ (1 << j)][k] + w[k][j]}
其中w[k][j]表示从点 k k k 到点 j j j 的距离,^表示异或运算。
state ^ (1 << j)是将 s t a t e state state 的第 j j j 位改变后的值,即

  • 如果 s t a t e state state 的第 j j j 位是 1 1 1 那么将其改为 0 0 0
  • 否则将 s t a t e state state 的第 j j j 位改为 1 1 1

由于到达点 j j j 的路径一定经过点 j j j,也就是说当 s t a t e state state 的第 j j j 位为 1 1 1 的时候, f [ s t a t e ] [ j ] f[state][j] f[state][j] 才可以被转移,所以 state ^ (1 << j) 其实就是将 s t a t e state state 的第 j j j 位改为 0 0 0,这样也就符合了 走到点 k k k 的路径就不能经过点 j j j 这个条件。
所有状态转移完后,根据 f [ s t a t e ] [ j ] f[state][j] f[state][j] 的定义,要输出 f [ 111 ⋯ 11 ( n 个 1 ) ] [ n − 1 ] f[111\cdots 11 (n个1)][n - 1] f[11111(n1)][n1]
那么怎么构造 n n n1 呢,可以直接通过 1 << n 求出 100 ⋯ 0 ( n 个 0 ) 100 \cdots 0(n个0) 1000(n0),然后减一即可。

时间复杂度
枚举所有 s t a t e state state 的时间复杂度是 O ( 2 n ) \mathcal O(2 ^ n) O(2n)
枚举 j j j 的时间复杂读是 O ( n ) \mathcal O(n) O(n)
枚举 k k k 的时间复杂度是 O ( n ) \mathcal O(n) O(n)
所以总的时间复杂度是 O ( n 2 2 n ) \mathcal O(n ^ 2 2 ^ n) O(n22n)

声明:
算法思路来源为y总,详细请见https://www.acwing.com/
本文仅用作学习记录和交流

你可能感兴趣的:(算法,算法,java,动态规划)