【状压dp】| 最短哈密顿距离

状压其实是一种很暴力的算法,因为他需要遍历每个状态,所以将会出现2n ,从0到2n -1 的情况数量,不过这并不代表这种方法不适用:一些题目可以依照题意,排除不合法的方案,使一行的总方案数大大减少从而减少枚举
状态压缩类动态规划,状压dp一般会有明显的数据范围特征,即数据的行数和列数n,m一般都在20以内
考虑到每行每列之间都有互相的约束关系。因此,我们可以用行和列描述2n种状态。用一个新的方法表示行和列的状态:数字。考虑任何一个十进制数都可以转化成一个二进制数,而一行的状态就可以表示成这样——例如:1010(2)
1表示有,0表示没有。
然后DP处理,枚举当前有效状态和上一行有效状态的关系。

91. 最短Hamilton路径

题目描述

给定一张 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

分析

一、状态表示:dp[i][j]表示从顶点0到顶点j,且经过的顶点的集合为i的所有路径。
dp[i][j]的值表示这些所有路径的和的最小值。

i 就是一个状态表示,用二进制数表示,假设i=10011,根据下标,低位到高位分别是0到4,即经过了顶点0、1、4。

二、状态计算: 根据dp[i][j]的含义,如果集合i的顶点里不包含终点j和起点0,那么这个值就没有含义,规定为无穷大。

例如,假如集合 i 等于11 (2),则表示该状态经过顶点0和顶点1,即直接求出0->1的距离即可。
如果集合i表示的状态是100 (2),那就表示该路径经过的顶点不过起点0,可直接判断为不存在。

因此求dp[i][j]的值,需要在状态i表示的集合经过起点0的情况下,即i&1!=0,去掉集合i中的顶点j,得到集合t,即t=i-{j},然后在集合t中寻找新的终点k,这时有dp[t][k],还需要顶点k到顶点j的代价g[k][j],这时只需要取min(dp[i][j],dp[t][k]+g[k][j]);就好了。

二进制i表示的十进制数肯定是要大于二进制t表示的十进制数的,求后面的状态dp[i][j]时要用到前面的状态dp[t][k],所以按照状态更新的拓扑序,应先枚举状态,再枚举到达的点。

即,对于二维表dp,应该按行更新,先求出每种路径状态i下可到达各个终点j的距离。

举个例子,按照我们的逻辑,在计算 f[101][1] (101是二进制下的数,点的编号从000开始)的时候,我们中间会用到 f[001][2] + g[2][1] 来更新该状态。 不会存在 f[111][1] 这样数据,求到达顶点1时,顶点1已经从集合i中去掉了
如果先枚举到达的点的话,我们会先计算 f[101][1],再计算 f[001][2]。那么我们在用 f[001][2] 更新f[101][1]的时候,由于f[001][2]还没计算过,所以还是正无穷,那么更新的 f[101][1]的值就是错误的。

最终结果:就是经过所有顶点,即状态111……11,且到达的顶点是n-1,即dp[(1<<n)-1][n-1]

三、初始化问题
dp[0][j],状态i为0,表示集合里不包括任何顶点,即 0->j 一个顶点也不经过,显然不可能,初始化为正无穷。dp[0][j]=INF;

dp[i][0] ,说明经过集合i的顶点后到达顶点0,

  1. 若i为1,只有顶点0被选中,说明集合i中只有顶点0,那么可以初始化为1;dp[1][0]=1;
  2. 若i大于0,说明在经过一系列顶点后还要到达顶点0,显然不可能,初始化为正无穷。dp[i][0]=INF

状态i等于二进制数1说明只经过顶点0,终点也为顶点为0,0->0的距离为0,且只能更新dp[1][0]这一个距离,

几个特殊情况:
假如路径的状态i是2^k时,即100……0,必不包括顶点0,所以到任何顶点的距离都不会更新。
假如路径的状态i是(2^k)+1时,即100……01,只包含顶点0和顶点k,那么只会更新0->k的直接距离,0为终点,k为起点,不经过其它顶点。

代码实现

#include <iostream>
#include <cstring>
#define read(x) scanf("%d",&x)

using namespace std;

const int N=20,M=1<<N;
int g[N][N],dp[M][N];

int main()
{
    
    
    int n;
    read(n);
    for (int i=0;i<n;i++)
        for (int j=0;j<n;j++) read(g[i][j]);
"初始化状态0和状态1,状态0均为INF"
    memset(dp,0x3f,sizeof dp);
    dp[1][0]=0;  "初始化状态为1的情况,由于只包含一个顶点,也只能初始化一个"
"开始递推"
    for (int i=2;i<(1<<n);i++) {
    
    "n个顶点,枚举2^n种状态,状态0和1都已经初始化过了,从状态2开始 "
        if (i&1==0)   continue; "不包含起点0的话直接下一个状态"
        for (int j=1;j<n;j++)  "当前状态i下,寻找每个点都做一次终点,顶点0就不用做终点了,已经初始化过了"
            if (i>>j&1) {
    
     "如果顶点j可以做顶点的话"
                int t=i-(1<<j); "减去该顶点j  ,i^(1<<j),异或也行"
                for (int k=0;k<n;k++) "从顶点0开始,再找一个新的顶点,且必须从0开始"
                    if (t>>k&1) dp[i][j]=min(dp[i][j],dp[t][k]+g[k][j]);
            }
    }
    printf("%d",dp[(1<<n)-1][n-1]);  
    return 0;
}

注意:

  1. 开始递推的第1个for循环,让状态从2开始是因为状态1已经初始化过了,也可以从1开始循环,由于集合中只有1个顶点,去掉该顶点作为终点后,集合t为0,不包含任何顶点,所以后面的都是空操作,相当于不执行。
  2. 开始递推的第2个for循环,寻找可以去掉的顶点,顶点0就不用去掉了,从顶点1开始查找到顶点n-1,因为从前面的分析中可以得知,在前面已经初始化过了。
  3. 开始递推的第3个for循环,寻找当前状态i的次顶点k,0->……->k,然后k->j,最后凑成0->j,顶点k必须要从顶点0开始找,因为可能存在前面说到过的100001(6位)这种状态,只用更新顶点0到顶点5的状态,其它顶点不存在也不能更新。

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/114484805