代码
一般集合
dp[1][0] = 0; //开始:集合中只有点0,起点和终点都是0
for (int S = 1; S < (1 << n); S++) { // 枚举每一种可能
for (int j = 0; j < n; j++) { // 枚举每一个点
if ((S >> j) & 1) { // 这个点存在
for (int k = 0; k < n; k++) {
if ((S ^ (1 << j)) >> k & 1) { // k属于集合S - j
dp[S][j] = min(dp[S][j], dp[S ^ (1 << j)][k] + dist[k][i]); // 中转
}
}
}
}
}
轮廓线(二维填充)
1、重点是对轮廓线附近的状态进行分析,得到状态转移方程
2、滚动数组(基本上固定的)
swap(n, m); // 复杂度为n * m * 2 ^ m , 所以m越小,越有利
// 初始化
dp[now][(1 << m) - 1] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
swap(now, old); // 滚动数组,now始终指向最新的一行
memset(dp[now], 0, sizeof dp[now]);
// 枚举这个点的上一行的所有状态
for (int k = 0; k < (1 << m); k++) {
if (k & 1 << (m - 1)) {
// k5 = 1时候x = 0 , k5为i,j上面的点(i - 1,j)的状态,1表示填充, 0表示没有填充,
if (k & 1 << (m - 1)) {
dp[now][(k << 1) & (~(1 << m))] += dp[old][k]; // k = k5 k4 k3 k2 k1 k0 ~(1 << m))这个的功能代表去除越界(长度超过6)的1
}
// k5 = 0 时候 x = 1,, i != 0 代表 i 不是第一行,第一行没法加上面的 k5 等于0 时候,x 不许等于1
if (i && !(k & 1 << (m - 1))) {
dp[now][(k << 1) ^ 1] += dp[old][k];
}
// x = 0,k5 = 1, k0 = 0 不能是第一列,第一列没法加左边的
if (j && (!(k & 1)) && (k & 1 << (m - 1))) {
dp[now][((k << 1) | 3) & (~(1 << m))] += dp[old][k];
}
}
}
}
}
// 最后的 dp[i - 1][j - 1][(k << m) - 1] 就是答案 从0开始存的
/* 解释
k = k5 k4 k3 k2 k1 k0 代表组合
k & 1 << (m - 1) 代表k5 = 1
(~(1 << m)) 这个的功能代表去除越界(长度超过6)的1
k << 1 最左边的一个k5不要了
(k << 1) ^ 1 ^ 是相同为0,不同为1的, 所以这句话是k左右边的变成1
((k << 1) | 3) 将k右边两个位都变成 1 (因为左右一起组合)
*/
原理
动态规划的题目中,状态的表示是解题的关键。在一些dp问题中,状态可能会非常复杂,导致用来存储状态的dp数组会有很多维。为此,我们需要用过状态压缩来达到减少状态维数的目的。在状态压缩dp中,灵活运用位运算是一项必不可少的要求。
核心
动态规划问题当中,复杂度等于状态数乘上决策数
状态数 = 集合的状态 + 我们现在所处的地方
if 判断 这个是否在状态里面
位运算
题目
1、旅行商(上面代码为此代码)
2、poj2411 (矩阵拼接)(轮廓线模式,上为代码)
3、三进制状态压缩 hdu3001
4、棋盘dp--最小三角路径
状态转移:
dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i - 1][j - 1])
练习:
洛谷p 1433 2831 2704 1879 1896 3092 3694 4925 2157 2167 2396 4363 5005 2150