AcWing 91. 最短Hamilton路径
题目描述
给定一张 n n n 个点的带权无向图,点从 0 ∼ n − 1 0 \sim n-1 0∼n−1 标号,求起点 0 0 0 到终点 n − 1 n-1 n−1 的最短 H a m i l t o n Hamilton Hamilton 路径。
H a m i l t o n Hamilton Hamilton 路径的定义是从 0 0 0 到 n − 1 n-1 n−1 不重不漏地经过每个点恰好一次。
输入格式
第一行输入整数 n n n。
接下来 n n n 行每行 n n n 个整数,其中第 i i i 行第 j j j 个整数表示点 i i i 到 j j j 的距离(记为 a [ i , j ] a[i,j] a[i,j])。
对于任意的 x , y , z x,y,z x,y,z,数据保证 a [ x , x ] = 0 , a [ x , y ] = a [ y , x ] a[x,x]=0,a[x,y]=a[y,x] a[x,x]=0,a[x,y]=a[y,x] 并且 a [ x , y ] + a [ y , z ] ≥ a [ x , z ] a[x,y]+a[y,z] \ge a[x,z] a[x,y]+a[y,z]≥a[x,z]。
输出格式
输出一个整数,表示最短 H a m i l t o n Hamilton Hamilton 路径的长度。
数据范围
1 ≤ n ≤ 20 1 \le n \le 20 1≤n≤20
0 ≤ a [ i , j ] ≤ 10 7 0 \le a[i,j] \le 10^7 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
解题思路
由于 n ≤ 20 n≤20 n≤20,无法用全排列枚举( 20 ! 20! 20! 太大)所有方案,需要使用状态压缩动态规划。
【状态定义】 d p [ s t a t e ] [ i ] dp[state][i] dp[state][i] 表示:当前已经访问过的点集为 s t a t e state state(二进制第 k k k 位为 1 1 1 表示点 k k k 已访问),并且最后停留在点 i i i 时的最短路径长度。
例如: s t a t e = 5 state = 5 state=5(二进制 101 101 101)表示访问了点 0 0 0 和点 2 2 2,最后在某个点。
【初始状态】 只访问了点 0 0 0,且最后在 0 0 0,长度为 0 0 0: d p [ 1 ] [ 0 ] = 0 dp[1][0] = 0 dp[1][0]=0。其余状态初始化为无穷大。
【状态转移】对于当前状态 s t a t e state state,假设最后到达的点是 j j j,那么上一个状态一定是 s t a t e state state 中去掉 j j j(即 s t a t e − ( 1 < < j ) state − (1<<j) state−(1<<j)),且上一个状态最后停留在某个点 k k k( k k k 必须是上一个状态中已访问的点)。
转移方程: d p [ s t a t e ] [ j ] = m i n ( d p [ s t a t e ] [ j ] , d p [ s t a t e − ( 1 < < j ) ] [ k ] + w [ k ] [ j ] ) dp[state][j] = min(dp[state][j], dp[state − (1<<j)][k] + w[k][j]) dp[state][j]=min(dp[state][j],dp[state−(1<<j)][k]+w[k][j])
【最终答案】 所有点都访问过: s t a t e = ( 1 < < n ) − 1 state=(1<<n)−1 state=(1<<n)−1,最后停在终点 n − 1 n−1 n−1,即 d p [ ( 1 < < n ) − 1 ] [ n − 1 ] dp[(1<<n)−1][n−1] dp[(1<<n)−1][n−1]。
参考代码
无注释
cpp
#include<bits/stdc++.h>
using namespace std;
const int N =20;
const int M = 1 << N;
int n, m[N][N], dp[M][N];
int main(){
cin >> n;
for(int i = 0; i < n; i ++)
for(int j = 0; j < n; j ++) cin >> m[i][j];
memset(dp, 0x3f, sizeof dp);
dp[1][0] = 0;
for(int i = 2; i < (1 << n); i ++)
for(int j = 0; j < n; j ++)
if((i >> j) & 1)
for(int k = 0; k < n; k ++)
if((i >> k) & 1)
if(j != k)
dp[i][j]=min(dp[i][j], dp[i - (1 << j)][k] + m[k][j]);
cout << dp[(1 << n) - 1][n - 1];
return 0;
}
有注释
cpp
#include <bits/stdc++.h> // 包含所有常用头文件
using namespace std;
const int N = 20; // 点的最大数量,因为n≤20
const int M = 1 << N; // 状态总数:每个点有经过/未经过两种状态,所以总状态数为2^n
int n; // 实际点的个数
int m[N][N]; // 邻接矩阵,存储两点之间的距离
int dp[M][N]; // dp[i][j]表示当前已经经过的点的集合为i,且当前停在点j的最短路径长度
int main() {
cin >> n; // 输入点的数量
// 读入邻接矩阵
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) cin >> m[i][j];
// 初始化dp数组为很大的数(0x3f3f3f3f表示无穷大)
memset(dp, 0x3f, sizeof dp);
// 初始状态:只经过了起点0,当前在点0,路径长度为0
// 二进制数1表示只有第0位是1(即只有点0被经过)
dp[1][0] = 0;
// 遍历所有可能的状态集合
// i表示当前经过的点的集合(用二进制表示,第k位为1表示点k已经过)
// 从2开始是因为状态1(只有起点)已经初始化,状态0(没有点)没有意义
for (int i = 2; i < (1 << n); i++)
// 遍历所有可能的当前所在点j
for (int j = 0; j < n; j++)
// 如果点j在当前经过的集合i中(即最后一步可以停在j)
if ((i >> j) & 1)
// 遍历所有可能的上一个点k
for (int k = 0; k < n; k++)
// 如果点k也在集合i中(即上一步是从k走到j)
if ((i >> k) & 1)
if(j != k) //最后一个点和倒数第二个点不相同
// 状态转移方程:
// 从集合i中去除点j(i - (1 << j)),上一个点是k
// 加上从k到j的距离m[k][j]
// 取所有可能的上一个点k中的最小值
dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + m[k][j]);
// 输出最终结果
// (1 << n) - 1 表示经过所有点的集合(二进制全1)
// n - 1 表示终点
// 即经过所有点且停在终点的最短路径长度
cout << dp[(1 << n) - 1][n - 1];
return 0;
}