状态压缩DP一般是基于二进制进行的,大家做这类题最好对此有所了解
状态压缩类动态规划(简称状压DP)也是一种很特殊的 DP 算法,其精髓就是将所有物品的状态(一般是选或不选,用01表示,当然也有特殊情况)压缩成一个整数,进行状态的转移并节约空间。
接下来我们来两道例题感受一下状态DP
eg:
分析题目:
(注:图片来自Acwing)
核心:我们的方案数==横着放的小方块数量
在这个题目里面我们要解决两个问题:(1)
自己手画的将就看看 别嫌弃!!!
假如i-2列和i-1列放的小方块和i-1和i列放的有重复的地方(如图中红绿相交的情况)就会产生冲突!
我们rp++肯定不会跟别人有冲突的说
如何解决呢?
我们可以用二进制来表示每一列的情况 如图中i-1列的情况就是1010100 我们用j来记录
所以状态表示f[i][j]:表示已经将前 i -1 列摆好,且从第i − 1 列,伸出到第 i 列的状态是 j 的所有方案。
(2)所有连续空着的的位置一定是偶数不能是奇数(如果是奇数这个位置竖着放不了)
状态转移:
我们可以来分析一下i-2和i-1与i-1和i列的状态转换关系;
我们用k来表示i-2伸出到i-1列的所有方案 例如(其他一个方案是01011)0代表着没有横着放方块;
加入j是i-1到i列的所有方案,那么k&j==0 意思就是不可以有相同的1不然就会出现冲突;
朴素的写法:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << N;
int n, m;
long long f[N][M];
bool st[M];
int main()
{
while (cin >> n >> m, n || m)
{
for (int i = 0; i < 1 << n; i ++ )
{
int cnt = 0;
st[i] = true;
for (int j = 0; j < n; j ++ )
if (i >> j & 1)
{
if (cnt & 1) st[i] = false;
cnt = 0;
}
else cnt ++ ;
if (cnt & 1) st[i] = false;
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= m; i ++ )
for (int j = 0; j < 1 << n; j ++ )
for (int k = 0; k < 1 << n; k ++ )
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
还有一种去掉无效状态 (更快)
#include <bits/stdc++.h>
using namespace std;
const int N = 12, M = 1<< N;
long long f[N][M] ;//第一维是列数,第二维是所有状态
bool st[M]; //判断每种状态连续空着的位置是否是偶数个
vector<vector<int>> state(M); //二维数组
int m, n;
int main() {
while (cin >> n >> m, n || m) {
//对于每种状态,先预处理每列不能有奇数个连续的0
for(int i = 0; i < (1 << n); i ++) {
int cnt = 0 ;//记录连续的0的个数
bool flag = true; // 某种状态没有奇数个连续的0则标记为true
for(int j = 0; j < n; j ++) { //遍历这一列,从上到下
if ( (i >> j) & 1) {
//i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位;
if (cnt & 1) {
//如果是奇数(cnt &1为真)则该状态不合法
flag =false; break;
}
cnt = 0;
}
else cnt ++; //该位还是0,则统计连续0的计数器++。
}
if (cnt & 1) flag = false; //最下面的那一段判断一下连续的0的个数
st[i] = flag; //状态i是否有奇数个连续的0的情况,输入到数组st中
}
//下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突
for (int j = 0; j < (1 << n); j ++) { //对于第i列的所有状态
state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
for (int k = 0; k < (1 << n); k ++) { //对于第i-1列所有状态
if ((j & k ) == 0 && st[ j | k])
// 第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行)
state[j].push_back(k);
}
}
//第三部分:dp
memset(f, 0, sizeof f);
//全部初始化为0,因为是连续读入,这里是一个清空操作。
//类似上面的state[j].clear()
f[0][0] = 1 ;
for (int i = 1; i <= m; i ++) { //遍历每一列:第i列合法范围是(0~m-1列)
for (int j = 0; j < (1<<n); j ++) { //遍历当前列(第i列)所有状态j
for (auto k : state[j]) // 遍历第i-1列的状态k,如果“真正”可行,就转移
f[i][j] += f[i-1][k]; // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
}
}
cout << f[m][0] << endl;
}
}
eg:
#include<bits/stdc++.h>
using namespace std;
const int N=20,M=1<<N;
int w[N][N];
int f[M][N];
int n;
int main()
{
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++) cin>>w[i][j];
memset(f,0x3f,sizeof f);
f[1][0]=0;
for (int i = 0; i < 1 << n; i ++ )
for (int j = 0; j < n; j ++ )
if (i >> j & 1)
for (int k = 0; k < n; k ++ )
if (i >> k & 1)
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
cout << f[(1 << n) - 1][n - 1];
return 0;
}
这道题比上面那到题简单 就不写题解啦