一、暴力穷举为什么不行
暴力来做的话,需要确定走的顺序,是一个\(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\) 两条路径,这两条路径有共同的状态:
-
所有点都经历过一遍
-
起点终点相同
用\(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;
}