状压dp:
前言:
前几周遇到了状压dp的题,不会,所以学习了一下状压dp,本想着上一周写一篇博客,但是由于太忙 懒 ,这周补一下…
1、位运算
这部分摘取自 传送门
名称 | 符号 | 运算法则 | 举例 |
---|---|---|---|
按位与 | a&b | 两者同时为1则为1,否则为0 | 00101&11100=00100 |
按位或 | a l b | 有1为1,无1为0 | 00101 l 11100=11101 |
按位异或 | a^b | 相同为0,不同为1 | 00101^11100=11001 |
按位取反 | ~a | 是1为0,是0为1 | ~00101=11010 |
左移 | a<<b | 把对应的二进制数左移b位 | 00101<<2=0010100 |
右移 | a>>b | 把对应的二进制数右移b位 | 00101>>1=0010 |
2、常用的计算方法
一、取出x的第i位:y = ( x>> ( i-1 ) ) & 1
二、将x的第i位取反:x = x ^ ( 1<< ( i-1 ) )
三、将x的第i位变为1:x = x | ( 1<< ( i-1 ) )
四、将x的第i位变成0:x = x & ~( 1<< ( i-1 ) )
五、将x最靠右的1变成0:x = x & (x-1)
六、取出最靠右的1:y=x&(-x)
七、把最靠右的0变成1: x | = (x-1)
3、状压dp
很显然,就是状态压缩 + 动态规划,我们在利用动态规划解题的时候,要定义dp状态,但是有时候dp状态维数太多,导致之间开多维数组的话会导致MLE,这时候就要用到状态压缩,利用二进制将其维数进行压缩,再按扎新的状态转移方程进行动态规划
至于如何进行状态压缩,主要是状态维数太多的时候进行压缩 废话,但是怎么压缩是没有定式的,下面给出几道题目来体会感受一下
例1、关灯问题
这道题目没有用到动态规划,只用到了状态压缩+BFS,作为状态压缩的引入和理解
这道题可以把这个灯开标记为1,关闭标记为0,这样就用一串二进制数表示所有灯的开关状态,这时候就可以利用bfs去跑了.同时我们可以发现,状态压缩只适用于小范围的数据,如果数据 > 64,那么unsigned long long
也存不下了
code
/***
* problem: P2622
* Author: Cu1
* time: 2020/12/2/21:59
***/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x7fffffff;
const int maxn = 1e2 + 5;
const int mod = 1e9 + 7;
const int dir8[8][2] = {{-1,0},{1,0},{0,-1},{0,1},{-1,-1},{-1,1},{1,-1},{1,1}};
const int dir14[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
int loc[maxn][maxn];
int deng[(1<<10) + 5];
int step[(1<<10) + 5];
bool vis[(1<<10) + 5];
queue <int> q;
int Solve(int m,int n)
{
int siz = 0;
//所有位全置1,表示所有灯全开
for(int i = 1; i <= n; i++) siz = siz<<1|1;
vis[siz] = true;
q.push(siz);
while(!q.empty()){
int now = q.front();
q.pop();
//now = 0表示灯处于全关状态
if(!now) return step[now];
for(int i = 1; i <= m; i++){
int ans = now;
//这个开关对每一盏灯的影响
for(int j = 1; j <= n; j++){
if(loc[i][j] == -1) ans = ans|1<<(j - 1);//这盏灯开启
if(loc[i][j] == 1) ans = ans&~(1<<(j - 1)); //这盏灯关闭
}
if(!vis[ans]){
q.push(ans);
vis[ans] = true;
step[ans] = step[now] + 1;
}
}
}
return -1;
}
int main()
{
int m,n;//开关数量,灯的数量
cin>>n>>m;
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
cin>>loc[i][j];
}
}
int if_locked = Solve(m,n);
cout << if_locked << endl;
}
例2、Corn Fields G
我们用1表示这块地可以种草,0表示这块地不可以种,那么一行一共有12列,所以每一行的二进制表示最多为1 << 12,且每一行的每一个状态都可以用上一行满足条件的状态转移过来得到
下面是如何判断状态合不合法:
1、判断同一行有没有相邻的土地有没有种植草:
将该状态左移(右移)一格与原状态取 & 操作,若 == 0则表示合法
2、有没有把草种在贫瘠的土地上,把原有土地状态用01表示出来,0为贫瘠,1为肥沃。将该状态与当前行土地状态取一下 | 操作,若等于原来土地状态,那么就为合法
3、判断是否上下相邻的草种在一起直接 当前行与上一行取 &= 0即为合法
注意:在dp时需要枚举考虑上一行状态
code
/***
* Problem: P1897
* Author: Cu1
* time 2020/12/02/21:56
***/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x7fffffff;
const int maxn = 12 + 5;
const int mod = 1e8;
const int dir8[8][2] = {{-1,0},{1,0},{0,-1},{0,1},{-1,-1},{-1,1},{1,-1},{1,1}};
const int dir14[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
ll dp[maxn][1 << 12];//dp[i,j]表示第i行以j状态时所有的种植方案数
//f[i]记录了第i行地能种的地方
int maps[maxn][maxn],f[maxn],vis[1 << 12];
bool check(int i)
{
if(i & (i >> 1)) return false;
if(i & (i << 1)) return false;
return true;
}
/***
* 有没有把草种植在贫瘠的土壤上?
*把原有土地的状态表示出来,0为贫瘠,1为肥沃,存下每一行的土地状态。将该状态与这一行的土地状态求一下交集,如果等于原状态,则合法
***/
ll Solve(int m,int n)
{
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
cin >> maps[i][j];
f[i] = f[i] | (maps[i][j] << j - 1);//记录当前行的土地的合法状态
}
}
int siz = 1 << n;
//枚举单独一行时合法的状态
for(int i = 0; i < siz; i++) {
if(check(i)) {
vis[i] = 1;
}
}
//第一行需要特判,因为它上面没有任何限制,只需要判断这一行是不是满足就可以
for(int i = 0; i < siz; i++) {
if(vis[i] && (f[1] | i) == f[1]) dp[1][i] = 1;
}
for(int i = 2; i <= m; i++) { //枚举第二行之后每一行
for(int j = 0; j < siz; j ++) { //枚举当前行状态
//判断是否合法
if(!vis[j] || (f[i] | j) != f[i]) continue;
//枚举上一行状态
for(int k = 0; k < siz; k++) {
if(!(k & j) && ((f[i - 1] | k) == f[i - 1])) dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;
}
}
}
ll res = 0;
for(int i = 0; i < siz; i++) {
res = (res + dp[m][i]) % mod;
}
return res;
}
int main()
{
int m,n;
cin >> m >> n;
ll res = Solve(m,n);
cout << res << endl;
return 0;
}
例3、互不侵犯
题意:
给定一个n*n的棋盘。在上面放k个王。满足这k个王无法相互攻击。王可以攻击他相邻的8个格子。
这个题其实就是例2的加强版,我们只需要开三个维度取存储dp状态即可
详细的解释都以注释写进代码里了
code
/***
* problem: P1896
* Author: Cu1
* time: 2020/12/2/22:00
***/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x7fffffff;
const int maxn = 1e2 + 5;
const int mod = 1e9 + 7;
const int dir8[8][2] = {{-1,0},{1,0},{0,-1},{0,1},{-1,-1},{-1,1},{1,-1},{1,1}};
const int dir14[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
//dp状态 dp[i,j,k] 表示 第i行 摆放国王状态为 j 一共花费了k个国王时共有多少种放法
ll dp[10][1<<10][100];
//f[i]表示当该行棋子状态为i时是否可以表示这一行的摆放方式
//num[i]表示当前行棋子摆放状态为i时,这一行有多少棋子
int f[1<<10],num[1<<10];
/***
*
* dp状态转移方程:
* dp[i,j,k] = sum( dp[i - 1,m,k - num[j]] )
* 其中,m是可以与j满足条件的上一行摆放状态,num[j]是摆放状态为j时,这一行所需要的棋子数量
*
***/
bool check(int a,int b)
{
if((a>>1)&b) return false;
if((a<<1)&b) return false;
if(a&b) return false;
return true;
}
int Solve(int n,int m)
{
//先预处理一下,判断每一行有多少种状态可行
int siz = 1<<n;
for(int i = 0; i < siz; i++) {
//如果有不满足条件的
if(((i<<1)&i)||((i>>1)&i)) continue;
f[i] = 1;
int tmp = i;
//统计合法状态下该行放置了几个棋子
while(tmp) {
if(tmp&1) num[i]++;
tmp = tmp>>1;
}
}
//单独特判第一行
for(int i = 0; i < siz; i++) {
if(f[i]) dp[1][i][num[i]] = 1;
}
for(int i = 2; i <= n; i++) { // 枚举第几行
for(int j = 0; j < siz; j++) { //枚举当前行状态
if(!f[j]) continue;
//枚举上一行状态
for(int k = 0; k < siz; k++) {
//检查枚举到的上一行是否合法,再判断这两行是否能放一起
if(!f[k]||!check(j,k)) continue;
//枚举到第i行总共有一共放置了多少棋子
for(int q = m; q >= num[j]; q--) {
dp[i][j][q] += dp[i - 1][k][q - num[j]];
}
}
}
}
ll ans = 0;
for(int i = 0; i < siz; i++) {
ans += dp[n][i][m];
}
return ans;
}
int main()
{
//n棋盘宽度,k国王数量
int n,k;
cin >> n >> k;
ll ans = Solve(n,k);
cout << ans << endl;
return 0;
}
例4、棋盘问题
这个就直接给出code吧
code
/***
* problem: 填满棋盘
* Author: Cu1
* time: 2020/12/2/21:59
***/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x7fffffff;
const int maxn = 1e3 + 5;
const int mod = 1e9 + 7;
const int dir8[8][2] = {{-1,0},{1,0},{0,-1},{0,1},{-1,-1},{-1,1},{1,-1},{1,1}};
const int dir14[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
int n,m;
ll dp[maxn][maxn];//dp[i,j] 铺满前i - 1列且第i行状态为j时的所有方案数
//枚举铺满当前列时,当前状态为state,对下一列影响为nex
void dfs(int c, int r,int state,int nex)
{
//最后一行铺满了
if( r == n + 1) {
dp[c + 1][nex] = ( dp[c + 1][nex] % mod + dp[c][state] % mod ) % mod;
return ;
}
//如果当前位置已经被上一行1 * 2的方块填上了,那么这一行就直接跳过
if((1<< r - 1) & state) {
dfs(c,r + 1,state,nex);
}
//如果这个位置没有被上一列的1 * 2的方块填上,就尝试放一个1 * 2的方块,并更新下一列的状态
if(!((1 << r - 1) & state)) {
dfs(c,r + 1,state,nex | (1 << r - 1));
}
//如果当前行且下一行都是空的,那么可以尝试放一个2 * 1的方块,而且不会对下一列产生影响
if(r + 1 <= n && !((1 << r - 1) & state) && !((1 << r) & state)) {
dfs(c,r + 2,state,nex);
}
}
int main()
{
while(cin >> n >> m) {
if(!n && !m) break;
memset(dp,0,sizeof dp);
dp[1][0] = 1;//初始化
for(int i = 1; i <= m; i++) {//枚举一列
for(int j = 0; j < (1 << n); j++) {//枚举第i - 1列对当前列的影响状态
if(dp[i][j]) {
//如果当前方案是可行的,那么就可以利用它去推出填满该列后对下一列的影响状态
dfs(i,1,j,0);
}
}
}
cout << dp[m + 1][0] << endl;
}
return 0;
}
例5、Problem Arrangement
题意:
输入n和m,接下来一个n*n的矩阵,a[i][j]表示第i道题放在第j个顺序做可以加a[i][j]的分数,问做完n道题所得分数大于等于m的概率。用分数表示,分母为上述满足题意的方案数,分子是总的方案数,输出最简形式。
我们可以算出,这道题的期望是
所有选法 / 满足条件数
这道题有两种写法,一种是递归,一种是迭代
其实我更倾向于使用递归,因为我的思维逻辑更倾向于一步一步选择,而不是把所有状态放一起进行枚举来得到最终结果
先给出递归的代码,很好理解,对于迭代的思路,下面会进行一些解释
code(递归):
/***
* title: ZOJ 3777
* author: Cu1
* time: 2020/12/7
***/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x7fffffff;
const int maxn = 13;
const int mod = 998244353;
const int dir8[8][2] = {{-1,0},{1,0},{0,-1},{0,1},{-1,-1},{-1,1},{1,-1},{1,1}};
const int dir4[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
int maps[maxn][maxn];
int dp[(1<<12) + 5][1500 + 3];
int n,m;
int gcd(int a,int b)
{
int temp;
while(b) {
temp = a % b;
a = b;
b = temp;
}
return a;
}
int Jiecheng(int n)
{
int res = 1;
for(int i = 1; i <= n; i++) res = res * i;
return res;
}
//state 选到第cnt个题时的状态以及此时兴趣值为val
int dfs(int state,int cnt,int val)
{
//如果这个状态之前以及选择过,就直接返回
if(dp[state][val] >= 0) return dp[state][val];
if(state == (1 << n) - 1) {
if(val >= m) return 1;
return 0;
}
int res = 0;
//枚举当前行时所有题目
for(int i = 1; i <= n; i++) {
//选到当前行且这个题没选到,就尝试选择这个题
if(!(state & (1 << i - 1))) {
res += dfs(state | 1 << i - 1,cnt + 1,val + maps[cnt][i]);
}
}
dp[state][val] = res;
return res;
}
int main()
{
int t;
cin >> t;
while(t--) {
cin >> n >> m;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
cin >> maps[i][j];
}
}
memset(dp,-1,sizeof dp);
//起始一道题没有选择,所以选择题目状态为0
//从第一个题开始选择,没有选择题目时兴趣值为0
int ans = dfs(0,1,0);
if(ans == 0) {
cout << "No solution" << endl;
continue;
}
int all = Jiecheng(n);
int g = gcd(ans,all);
cout << all / g << "/" << ans / g << endl;
}
return 0;
}
关于迭代的思路:
由于每一个状态都是前面状态转移来的,所以我们可以从小到大枚举所有状态,并把它可以转移到的状态加上当前状态的数量即可,而且由于每一次状态的转移都会生成一个比当前大的二进制数,所以是不需要担心转移后状态反过来影响当前状态的数量的,也就满足了动态规划的无后效性
这个代码引自传送门
code
#include<bits/stdc++.h>
using namespace std;
int casen;
int n,m;
int a[15][15];
int dp[(1<<13)+10][510];
int f[15];
int gcd(int a,int b)
{
if(b==0) return a;
return gcd(b,a%b);
}
int main()
{
f[1]=1;
for(int i=2;i<=12;i++)//计算阶层
f[i]=f[i-1]*i;
scanf("%d",&casen);
while(casen--)
{
memset(dp,0,sizeof(dp));
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&a[i][j]);
dp[0][0]=1;
for(int i=0;i<(1<<n);i++)
{
int cnt=0;
for(int j=1;j<=n;j++)
{
if(((1<<(j-1))&i)>0)//判断i的二进制下第j为是否为1
cnt++;
}
for(int j=1;j<=n;j++)//看看可以由i状态转移到哪些别的状态
{
if(((1<<(j-1))&i)>0)
continue;
for(int k=0;k<=m;k++)
{
if(k+a[cnt+1][j]>=m)
dp[i+(1<<(j-1))][m]+=dp[i][k];
else
dp[i+(1<<(j-1))][k+a[cnt+1][j]]+=dp[i][k];
}
}
}
if(dp[(1<<n)-1][m]==0)
puts("No solution");
else
{
int g=gcd(f[n],dp[(1<<n)-1][m]);
printf("%d/%d\n",f[n]/g,dp[(1<<n)-1][m]/g);
}
}
//system("pause");
return 0;
}
例6、炮兵阵地
题意很清晰:这里就不多追述
这个题就是例2和例3的加强版
首先我们先定义dp状态dp[i,j,k] 第i行状态为k前一行状态为j时摆放的炮兵数量
,可是由于n为100且后两维的大小为1 << 10这样一定为MLE的,但是由于炮兵范围是一个十字,所以我们要先把同一行内如果有两个炮兵相邻距离 < 3的给舍去,如果打印m = 10时,我们可以发现,满足条件的数量最多不过70,那么我们可以开一个数组把这些状态记录下来,然后dp后两维状态以满足条件状态的数量,就不会MLE了,当然还有大佬用滚动数组去防止MLE,可以去洛谷看一下大佬们的题解
由于dp当前行状态与前两行状态有关,所以我们要先把前两行的所有状态提前预处理一遍,而且在后续枚举状态中,我们可以得出状态转移方程:
dp[i][j][k] = max ( dp[i][j][k],dp[i-1][p][j] +当前行的炮兵数量 )
我们每一步要枚举当前行的状态和相对应满足条件的上两行所有状态
更具体可以看下代码注释
code
/* **
* title: 炮兵阵地
* author: Cu1
* time:2020/12/8
* **/
/* **
* title: 炮兵阵地
* author: Cu1
* time:2020/12/8
* **/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x7fffffff;
const int maxn = 1e2 + 5;
const int mod = 998244353;
const int dir8[8][2] = {{-1,0},{1,0},{0,-1},{0,1},{-1,-1},{-1,1},{1,-1},{1,1}};
const int dir4[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
int maps[maxn];//地图状态
//dp[i,j,k]第i行时上一行状态为j,当前状态为k时的最大方案数
int dp[maxn][maxn][maxn];
int state[maxn],number[maxn];//单独一行满足条件时的状态和平原数
bool Judge(int i)
{
//判断是否有两个炮兵阵地距离小于2
if(i & (i << 1)) return false;
if(i & (i << 2)) return false;
if(i & (i >> 1)) return false;
if(i & (i >> 2)) return false;
return true;
}
//判断情况y与地形x是否冲突
bool check(int x,int y)
{
if((x | y) != x) return false;
return true;
}
//计算当前状态中有几个1,也即当前状态下放了多少炮兵
int num(int x)
{
int count = 0;
while(x) {
if(x & 1) count++;
x = x >> 1;
}
return count;
}
void Solve(int n,int m)
{
int siz = 1 << m;
int cnt = 0;
//预处理一行中所有合法摆放炮兵状态
for(int i = 0; i < siz; i++) {
if(Judge(i)) state[cnt] = i,number[cnt] = num(i),cnt++;
}
//给第一行赋初值
for(int i = 0; i < cnt; i++) {
if(check(maps[1],state[i])) dp[1][0][i] = number[i];
}
//枚举第二行
for(int i = 0; i< cnt; i++) {
for(int j = 0; j < cnt; j++) {
//与地形不符
if(!check(maps[2],state[j])) continue;
//防止两个炮兵在同一列
if(state[j] & state[i]) continue;
dp[2][i][j] = max(dp[2][i][j],dp[1][0][i] + number[j]);
}
}
//枚举当前行
for(int i = 3; i <= n; i++) {
//枚举当前行状态
for(int j = 0; j < cnt; j++) {
//如果与地形冲突,就不选择这个状态
if(!check(maps[i],state[j])) continue;
//枚举前一行状态
for(int k = 0; k < cnt; k++) {
//如果这两行中有某一列放置了炮兵,就不能选择这个状态
if(state[k] & state[j]) continue;
//枚举前两行状态
for(int q = 0; q < cnt; q++) {
//不与前两行冲突且前两行自身不冲突
if(state[q] & state[j]) continue;
if(state[q] & state[k]) continue;
dp[i][k][j] = max(dp[i][k][j],dp[i - 1][q][k] + number[j]);
}
}
}
}
int ans = -inf;
//枚举最后两行状态找到最大值
for(int i = 0; i < cnt; i++) {
for(int j = 0; j < cnt; j++) {
ans = max(dp[n][i][j],ans);
}
}
cout << ans << endl;
}
int main()
{
int n,m;
cin >> n >> m;
memset(maps,0,sizeof maps);
for(int i = 1; i <= n; i++) {
string s;
cin >> s;
//记录每一行状态
for(int j = 1; j <= m; j++) {
if(s[j - 1] == 'P') maps[i] = maps[i] | 1 << j - 1;
}
}
Solve(n,m);
return 0;
}
`