题目:
给定一张 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 是一个二进制数,表示点集的方法如上述所示
- 集合:经过的点集为 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[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/
本文仅用作学习记录和交流