AcWing 91. 最短Hamilton路径(状态压缩DP)

 

给定一张 nn 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。

输入格式

第一行输入整数nn。

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

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

输出格式

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

数据范围

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)
首先想下暴力算法,这里直接给出一个例子。
比如数据有 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,可以发现,我们在计算从点 00 到点 33 的路径时,其实并不关心这两中路径经过的点的顺序,而是只需要这两种路径中的较小值,因为只有较小值可能对答案有贡献。
所以,我们在枚举路径的时候,只需要记录两个属性:当前经过的点集,当前到了哪个点。
而当前经过的点集不是一个数。观察到数据中点数不会超过 2020,我们可以用一个二进制数表示当前经过的点集。其中第 ii 位为 1/0 表示是/否经过了点 ii。
然后用闫式 dp 分析法考虑 dp
状态表示:f[state][j]。其中 statestate 是一个二进制数,表示点集的方法如上述所示。

集合:经过的点集为 state,且当前到了点 jj 上的所有路径。
属性:路径总长度的最小值
状态计算:假设当前要从点 k转移到 j。那么根据 Hamilton 路径的定义,走到点 kk 的路径就不能经过点 j,所以就可以推出状态转移方程f[state][j] = min{f[state ^ (1 << j)][k] + w[k][j]}
其中w[k][j]表示从点 k 到点 j 的距离,^表示异或运算。
state ^ (1 << j)是将 statestate 的第 jj 位改变后的值,即

如果 state 的第 j 位是 1 那么将其改为 0
否则将 state的第 j 位改为 1
由于到达点 j 的路径一定经过点 j,也就是说当 state 的第 j 位为 1 的时候,f[state][j] 才可以被转移,所以 state ^ (1 << j) 其实就是将 state 的第 j 位改为 0,这样也就符合了 走到点 k 的路径就不能经过点 j 这个条件。

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

时间复杂度

枚举所有 state 的时间复杂度是 O(2^n)
枚举 jj 的时间复杂读是 O(n)
枚举 kk 的时间复杂度是 O(n)
所以总的时间复杂度是 O(n^2 * 2^n)

作者:垫底抽风
链接:https://www.acwing.com/solution/content/15328/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


import java.io.*;
import java.util.*;
import java.lang.*;

public class Main{
    
    static int n = 0, k = 0, N = 20, M = 1 << N;
    static int[][] f = new int[M][N];
    static int[][] w = new int[N][N];
    public static void main(String[] args) throws Exception{
        BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.valueOf(buf.readLine());
        for (int i = 0; i < n ; i++){
            String[] info = buf.readLine().split(" ");
            for (int j = 0 ; j < n ; j++){
                w[i][j] = Integer.valueOf(info[j]);//存储节点距离
            }
        }
        for (int i = 0; i < (1 << n); i++){//给节点距离数组初始化为无穷大
            Arrays.fill(f[i], 0x3f3f3f3f);
        }
        f[1][0] = 0;//从0节点开始,所以第0位必须事先设置,表示0节点到0节点的距离为0
        for (int i = 1 ; i < (1 << n); i++){//枚举0~11111...111个状态
            for (int j = 0 ; j < n ; j++){//中间经过的点
                if (((i >> j) & 1) != 0){//表示可以到达第j个节点,否则不能到达怎么去更新?
                    for (int k = 0 ; k < n ; k ++){//枚举中间节点k,使得k节点到j节点,找到最小距离并更新
                        if ((((i ^ (1 << j)) >> k )& 1 ) != 0){
                            //由于要更新j位置,所以之前的点集不能经过j,
                            //因此i ^ (1 << j)表示将当前j为1时修改为0,表示选择那些没经过j节点的状态
                            f[i][j] = Math.min(f[i][j], f[i ^ (1 << j)][k] + w[k][j]);
                        }
                    }
                }
            }
        }
        System.out.print(f[(1 << n) -1 ][n-1]);
    }
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值