最短Hamilton路径

题目描述
给定一张 nn 个点的带权无向图,点从 0∼n−10∼n−1 标号,求起点 00 到终点 n−1n−1 的最短 HamiltonHamilton 路径。 HamiltonHamilton 路径的定义是从 00 到 n−1n−1 不重不漏地经过每个点恰好一次。

输入格式
第一行输入整数 nn。

接下来 nn 行每行 nn 个整数,其中第 ii 行第 jj 个整数表示点 ii 到 jj 的距离(记为 a[i,j]a[i,j])。

对于任意的 x,y,zx,y,z,数据保证 a[x,x]=0a[x,x]=0,a[x,y]=a[y,x]a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]a[x,y]+a[y,z]≥a[x,z]。

输出格式
输出一个整数,表示最短 HamiltonHamilton 路径的长度。

数据范围
1≤n≤201≤n≤20
0≤a[i,j]≤1070≤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) O(n2)O(n2)
首先想下暴力算法,这里直接给出一个例子。
比如数据有 5 个点,分别是 0,1,2,3,4 只算对答案有贡献的情况的话):

case 1: 0→1→2→3→4
case 2: 0→1→3→2→4
case 3: 0→2→1→3→4
case 4: 0→2→3→1→4
case 5: 0→3→1→2→4
case 6: 0→3→2→1→4
那么观察一下 case 1 和 case 3 ,可以发现,我们在计算从点 0 到点 3 的路径时,其实并不关心这两中路径经过的点的顺序,而是只需要这两种路径中的较小值,因为只有较小值可能对答案有贡献。
所以,我们在枚举路径的时候,只需要记录两个属性:当前经过的点集,当前到了哪个点。
而当前经过的点集不是一个数。观察到数据中点数不会超过 2020,我们可以用一个二进制数表示当前经过的点集。其中第 i 位为 1/0 表示是/否经过了点 i。
然后用闫式 dp 分析法考虑 dp
状态表示:f[state][j] 。其中 statestate 是一个二进制数,表示点集的方法如上述所示。

集合:经过的点集为 statestate,且当前到了点 j 上的所有路径。
属性:路径总长度的最小值
状态计算:假设当前要从点 k 转移到 j。那么根据 HamiltonHamilton 路径的定义,走到点 k 的路径就不能经过点 j,所以就可以推出状态转移方程f[state][j] = min{f[state ^ (1 << j)][k] + w[k][j]}
其中w[k][j]表示从点 kk 到点 jj 的距离,^表示异或运算。
state ^ (1 << j)是将 statestate 的第 j 位改变后的值,即
如果 statestate 的第 j 位是 1 那么将其改为 0
否则将 statestate 的第 j 位改为 1
由于到达点 jj 的路径一定经过点 jj,也就是说当 statestate 的第 j 位为 1 的时候,f[state][j] 才可以被转移,所以 state ^ (1 << j) 其实就是将 statestate 的第 j 位改为 0,这样也就符合了 走到点 k 的路径就不能经过点 j 这个条件。

所有状态转移完后,根据 f[state][j] 的定义,要输出 f[111⋯11(n个1)][n−1] 。
那么怎么构造 nn 个 1 呢,可以直接通过 1 << n 求出 100⋯0(n个0) ,然后减一即可。

#include <stdio.h>
#include <string.h>

const int N = 20;
const int M = 1 << 20; // 一共最多有 20 个 1 种状态

int n;
int w[N][N];           // 存每两个点之间的距离
int f[M][N];           // 上述 f[state][j]

int main()
{
   scanf("%d", &n);
   for (int i = 0; i < n; i ++ )
       for (int j = 0; j < n; j ++ )
           scanf("%d", &w[i][j]);
   memset(f, 0x3f, sizeof f); // 由于要求最小值,所以这里将 f 初始化为正无穷会更好处理一些
   f[1][0] = 0;               // 因为题目要求从点 0 出发,所以这里要将 经过点集为 1,当前到达第 0 个点 的最短路径初始化为 0
   for (int state = 1; state < 1 << n; state ++ )   // 从 0 到 111...11 枚举所有 state
       if (state & 1)                               // state 必须要包含起点 1
           for (int j = 0; j < n; j ++ )            // 枚举所有 state 到达的点
               if (state >> j & 1)                  // 如果当前点集包含点 j,那么进行状态转移
                   for (int k = 0; k < n; k ++ )    // 枚举所有 k
                       if (state ^ 1 << j >> k & 1) // 如果从当前状态经过点集 state 中,去掉点 j 后,state 仍然包含点 k,那么才能从点 k 转移到点 j。
                                                    // 当然这个 if 也可以不加,因为如果 state 去掉第 j 个点后,state 不包含点 k 了,
                                                    // 那么 f[state ^ 1 << j][k] 必然为正无穷,也就必然不会更新 f[state][j],所以去掉也可以 AC。
                           if (f[state ^ 1 << j][k] + w[k][j] < f[state][j]) // 由于 >> 和 << 的优先级要比 ^ 的优先级高,所以这里可以将 state ^ (1 << j) 去掉括号。
                               f[state][j] = f[state ^ 1 << j][k] + w[k][j];
   printf("%d\n", f[(1 << n) - 1][n - 1]);          // 最后输出 f[111...11][n-1]
   return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值