【笔记】洛谷P10447 最短 Hamilton 路径(状压dp)
题意
忽然懒得写了,请看原题(bushi
思路
根据题目对最短汉密尔顿路径的定义,我们需要求出从 \(0\) 号点开始不重不漏经过所有点,最后到达 \(n-1\) 号点的最短路径。
如果使用回溯暴力的方法,显然时间复杂度过高——有 \((n-2)!\) 条路径,每条路径都需要枚举边权计算总和,一条路径是 \(O(n-1)\),所有路径就是 \(O((n-2)!\cdot (n-1))\),可粗略写成 \(O(n!\cdot n)\),必然超时。
于是可以想到使用动态规划,令 \(dp[i][j]\) 表示从 \(0\) 号点开始不重不漏经过 \(i\) 集合中所有点、并最终停留在 \(j\) 点上的最短路径。
0 -> 1 -> 2 -> 3 (长度为 5)
0 -> 2 -> 1 -> 3 (长度为 10)
上面两条路径均经过集合 {0,1,2,3} 中所有点,并最终停留在 3 号点上,且第一条优于第二条。因此 dp[{0,1,2,3}][3]=5。
最后 \(dp[\{0,1,2,\dots ,n-1\}][n-1]\) 即为所求。
状态压缩
但是如何使用一个数(即上文中 \(i\))表达一个集合?
每个点都有“在集合中”和“不在集合中”两种状态,我们用“1”表示前者,“0”表示后者。将它们按顺序摆放,就得到了一个二进制数,将其转换为十进制,即是 \(i\)。
现有八个点,编号为 0~7。
00000000 {} 没有点在集合中
00000001 {0} 0 号点在集合中
00000011 {0,1} 0、1 号点在集合中
01110001 {0,4,5,6} 0、4、5、6 号点在集合中
用一位二进制数表示一个点的状态,一个 \(n\) 位的二进制数即可表示整个集合,用 (i>>k)&1 判断 \(i\) 的第 \(k\) 位是否为 1,也就是判断 \(k\) 号点是否存在于集合 \(I\) 中。\(n\) 最大为 20,没有超过 int 的范围。遍历 \([0,2^n-1]\) 等于遍历所有状态。
状态转移
我们只需要关注“当前已经过哪些点(用集合表示)”和“当前停留在哪个点”。
首先枚举每一个状态,对于该状态,再枚举集合中的每一个点,再对于这个点,枚举它所有可能的前驱并挑选一个最短的填入 \(dp\) 数组。
\(dp[i][j]=\min_{u\in I}dp[I-\{u\}][u]+g[u][j]\)
根据对 \(dp\) 数组的定义,必须满足 \(0\in I,j\in I\)。
时间复杂度
\(2^n\) 个状态、每个状态最多 \(n\) 个点、每个点需遍历最多 \(n\) 个前驱,总复杂度为 \(O(2^n\cdot n^2)\)
代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int g[25][25];//邻接矩阵
int n;
int dp[(1<<20)+5][25];//最多有 20 个点,数组至少开到 1<<20
int main(){
cin>>n;
for(int i=0;i<n;i++) for(int j=0;j<n;j++) cin>>g[i][j];
memset(dp,0x3f,sizeof(dp));//因为求最小值,所以初始化为极大值
dp[1][0]=0;//从起点走到起点长度为 0
int m=1<<n;//状态数量
for(int i=1;i<m;i+=2){////遍历每一个状态;0 号点必须存在于集合中,当 i 为偶数时必然不符合要求
for(int j=0;j<n;j++){//遍历每一个点
if((i>>j)&1){//如果 j 点在 i 集合中
for(int k=0;k<n;k++){//遍历所有可能的前驱
if(((i^(1<<j))>>k)&1){//若 k 是合法的前驱,则进行大小判断;i^(1<<j) 表示从 i 中去除 j 点后的集合
dp[i][j]=min(dp[i][j],dp[i^(1<<j)][k]+g[k][j]);//尝试更新 dp[i][j]
}
}
}
}
}
cout<<dp[m-1][n-1];
return 0;
}

浙公网安备 33010602011771号