题目:
给定一张 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: 0→1→2→3→4
c a s e 2 : 0 → 1 → 3 → 2 → 4 case\ 2:\ 0 \rightarrow 1 \rightarrow 3 \rightarrow 2 \rightarrow 4 case 2: 0→1→3→2→4
c a s e 3 : 0 → 2 → 1 → 3 → 4 case\ 3:\ 0 \rightarrow 2 \rightarrow 1 \rightarrow 3 \rightarrow 4 case 3: 0→2→1→3→4
c a s e 4 : 0 → 2 → 3 → 1 → 4 case\ 4:\ 0 \rightarrow 2 \rightarrow 3 \rightarrow 1 \rightarrow 4 case 4: 0→2→3→1→4
c a s e 5 : 0 → 3 → 1 → 2 → 4 case\ 5:\ 0 \rightarrow 3 \rightarrow 1 \rightarrow 2 \rightarrow 4 case 5: 0→3→1→2→4
c a s e 6 : 0 → 3 → 2 → 1 → 4 case\ 6:\ 0 \rightarrow 3 \rightarrow 2 \rightarrow 1 \rightarrow 4 case 6: 0→3→2→1→4
那么观察一下 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 是一个二进制数,表示点集的方法如上述所示
状态计算:假设当前要从点 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 位改变后的值,即
由于到达点 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[111⋯11(n个1)][n−1]。
那么怎么构造 n n n 个 1
呢,可以直接通过 1 << n
求出 100 ⋯ 0 ( n 个 0 ) 100 \cdots 0(n个0) 100⋯0(n个0),然后减一即可。
时间复杂度
枚举所有 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/
本文仅用作学习记录和交流