• AcWing 91. 最短Hamilton路径


    题目传送门

    一、暴力穷举为什么不行

    暴力来做的话,需要确定走的顺序,是一个(0)(n-1)的全排列 ,假设(n=5) .就是(0)号到(4)号,共(5)个节点。
    $0 1 2 3 4 $
    $0 1 3 2 4 $
    $0 2 1 3 4 $
    $0 2 3 1 4 $
    $0 3 1 2 4 $
    $0 3 2 1 4 $
    (6)组,个数是((n-2)!)个,(在为首尾是固定的,其它的是全排列)

    如果要暴力解决,那么每一组解都需要遍历一次所有节点的权,就是需要一重循环加上去,是(n)个,就是(n * (n-2)!)个。(n)要是(20)左右的数,就很恐怖了。

    二、动态规划为什么行

    动态规划从本质上讲,并没有真正算出所有的可行解是什么,而一般是计算 最大值,最小值,总方案数等,说白了,就是只计算数量,而不是真的列出来到底是哪些,举个栗子就是老师让孩子数一下(1)(100)有多少个数,有的孩子是掰手指头一个个查,最终答案是(100),有的孩子聪明,知道(1)(100)(100)个数是一个道理。

    三、为什么要用状态压缩

    某类问题包含很多的信息,每一个信息都需要一个数组来存储。

    例如:有一道题是关于(n)扇门的状态的问题,有(5)个门,(1)代表开,(0)代表关。那么用数组描述(a[1]—a[5]):分别为$0 1 1 0 1 $ 就代表 关 开 开 关 开,如果(n)是一个很大的数(10^8),数组往往开的太大了!

    所以呢,为了避免空间开太大,也为了方便程序描述状态,可以把这个状态压缩成一个十进制的数字(13)来代替,因为((13)_2=01101)

    另外,如果使用二进制描述的话,那么 左移,右移,与,或,非,异或等操作就是非常自然的,可以很灵活找出两个状态之间的交集,并集等,非常方便。

    四、按DP思考问题

    对于一个(4)个点的带权无向图,从点(1)到点(4)有两条路径,(1->2->3->4)(1->3->2->4) 两条路径,这两条路径有共同的状态:

    1. 所有点都经历过一遍

    2. 起点终点相同

    (f[i][j]) 表示经过(i)这些点,终点是(j)的最短长度。

    我们用一个(2^n)(从(0)(2^n-1))个数(i)的每一位表示该点是否经过。
    其中(i)的用例以(1-4)为例,就是如下:
    (0000,0001,0010,0100,1000,0011,0101,1100,1001,1010,0110,1111,1110,1101,1011,0111) 全排列,共(16)个。

    这里面有些状态是不符合条件的,我们会在后面的判断中去除掉,比如想求从(1)(3)的所有状态,结果路径中没有出现(1)(3),就肯定不是合法状态。

    注意,这里随着j的变化(i)的合法状态也是不一样的。

    (k->j) 代表的是走的(k)(j)这条边,就是(w[k][j])就是从(k)走到(j)的距离。

    我们想要整个(f[i][j])最短,而 (w[k][j])是固定的,那么就需要前面的最短,就是(0-->k)最短

    我们从(0-->j)走过的是(i)

    我们从(0-->k)走的是(i-(1 << j)),这个太棒了,怎么想的啊!!!!

    (f[i,j]= min(f[i,j],f[i-(1 << j),k]+a[k][j]))

    五、实现代码

    #include <bits/stdc++.h>
    
    using namespace std;
    const int N = 20;       //好小的上限N,大的没法状态压缩实现,2^N不能太大啊!
    const int M = 1 << N;   //2的N次方
    int w[N][N];            //邻接矩阵,记录每两个点之间的距离
    int f[M][N];            //DP状态数组,记录每一步的最优解
    int n;                  //n个结点
    
    int main() {
        //优化输入
        ios::sync_with_stdio(false);
        cin >> n;
    
        //读入邻接矩阵,注意是从0~n-1
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                cin >> w[i][j];
    
        //求最短,初始正无穷
        memset(f, 0x3f, sizeof f);
    
        // 初始化
        // 从0号点出发,到达0号点,已经走过的路径就只有0这一个结点,用二进制描述的话,就只有一位,那么填1还是填0呢?
        // 答案:填1,因为0这个点其实是走过了的。
        f[1][0] = 0;
    
        //枚举状态
        for (int i = 0; i < (1 << n); i++)
            //枚举每个节点
            for (int j = 0; j < n; j++)
                //这个状态中第j位是不是1,判断这个节点是不是包含在路径中,不在路径中的没有必要进行处理
                if (i >> j & 1) {
                    //如果通过引入结点k,使得距离更短的话
                    for (int k = 0; k < n; k++)
                        //需要满足i这个路径中除去j这个点,一定要包含k这个点
                        if ((i - (1 << j)) >> k & 1)
                            f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
                }
        // 最终经历了所有结点,并且最后停在n-1(最后一个点,因为坐标从0开始)这个点
        printf("%d", f[(1 << n) - 1][n - 1]);
        return 0;
    }
    
  • 相关阅读:
    [LeetCode] 1190. Reverse Substrings Between Each Pair of Parentheses
    [LeetCode] 923. 3Sum With Multiplicity
    [LeetCode] 643. Maximum Average Subarray I
    [LeetCode] 669. Trim a Binary Search Tree
    [LeetCode] 1743. Restore the Array From Adjacent Pairs
    [LeetCode] 888. Fair Candy Swap
    [LeetCode] 1102. Path With Maximum Minimum Value
    [LeetCode] 1631. Path With Minimum Effort
    [LeetCode] 1522. Diameter of N-Ary Tree
    [LeetCode] 1376. Time Needed to Inform All Employees
  • 原文地址:https://www.cnblogs.com/littlehb/p/15468831.html
Copyright © 2020-2023  润新知