题目传送门

一、暴力穷举为什么不行

暴力来做的话,需要确定走的顺序,是一个\(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;
}

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2021-07-16
  • 2021-07-26
  • 2022-12-23
  • 2022-01-18
猜你喜欢
  • 2021-09-11
  • 2022-12-23
  • 2021-11-17
相关资源
相似解决方案