状压dp的含义
在我们解决动态规划题目的时候,dp数组最重要的一维就是保存状态信息,但是有些题目它的具有dp的特性,并且状态较多,如果直接保存的可能需要三维甚至多维数组,这样在题目允许的内存下势必是开不下的,那么我们能不能想个办法,把它压缩成一维呢?对,二进制.一般的动规题目数据范围都不会太大,那么就可以把几个状态全部压缩成一个二进制数保存下来,这样就大大节省了空间,来允许我们进行其他的操作,这就叫做状态压缩.运用状态压缩来保存状态的dp就叫做状压dp,这类dp一般数据范围有一项很小(好像是不超过16吧),看到这种数据范围就可以往状压上想
纸上谈兵是没用的,下面我们来看一道例题
题目大意:农夫有一块地,被划分为m行n列大小相等的格子,其中一些格子是可以种植的(用1标记),农夫可以在这些格子里种植,其他格子则不能种植(用0标记),并且要求不可以使相邻格子都被种植。现在输入数据给出这块地的大小及可否种植的情况,求该农夫有多少种种植方案可以选择(注意:任何格子都不种植也是一种选择,不要忘记考虑!)
解题思路:按照刚才我说的,题目中的m,n最大都只有12,我们要很快想到状压dp,那么如何状压呢?其实状压dp就是一种枚举,是最暴力的一种dp.
在题目中,有1的地方就可以种植,否则不行,在不考虑时间复杂度的情况下,我们是不是会想到打暴搜,枚举每一种情况,如果一块地上已经种了草,那么上下左右就都不能种了.我们经这种思路转化成二进制,1代表在这块地上种植,0代表不种,例如:010就代表在第二块地种植,其他地都不种.
我们枚举每一行的状态,在左右不相邻的情况下,再判断下一行不和本行状态冲突的状态(如:第一行是0 1 0,第二行是0 1 0就冲突了,即上下行同一位置不能同时种植),这样我们只需要预处理出第一行的状态就可以递推出其他行的所有满足条件的状态个数了
下面来分析一下题目样例
1 1 1
0 1 0
第一行满足条件的状态有
1 | 0 0 0 |
2 | 1 0 0 |
3 | 0 1 0 |
4 | 0 0 1 |
5 | 1 0 1 |
第二行满足条件的状态有
1 | 0 0 0 |
2 | 0 1 0 |
根据乘法原理有5*2=10种方法,但其中一种第一行0 1 0和第二行0 1 0是冲突的,所以结果为10-1=9种方案
设计dp数组的状态,状压dp状态应该还是比较好设计的,本题为dp[i][state[j]]表示到第i行到第j种状态满足条件的方案数
满足无后效性原则,下一行的状态只能由前一行转移过来
dp[i+1][state[j]]+=dp[i][state[k]] state[k]表示第i行满足条件的状态
总结一下思路:先枚举第一行,把所有可能的状态和第一行的题目所给环境对比,如果成功,则在循环里继续枚举第二行,把所有可能的状态和第二行的环境对比,如果成功,再和第一行填入的状态对比,如果又匹配成功,则dp[2][000] = dp[2][000] + dp[1][100];方法数加到第二行。这就是一次循环结束了,重新枚举第二行...
//cur[i]表示第i行的环境
for(int i=;i<=m;i++) {
for(int j=;j<=n;j++) {
int a; in(a);//输入环境
if(!a) cur[i]|=(<<(n-j));//这个有两点要注意,一个是所有的0变成1,1变成0(这个必须),一个是反向(正向也可以)存环境------>cur[i]|=(1<<(j-1));
}
}
我们假设一下如果不是0,1互换,那么我们后面判断它是否合法时就会出现问题,比如我第1行的状态为1 0 1,互换后为0 1 0,在后面的程序中有这样一条判断是否合法的语句
if((can[i] & cur[j])==0) 代表它合法--------互换后的程序
不互换的话就是这样 if(can[i] & cur[j]) 乍一看好像没什么不对,但是我们考虑一种情况,就是当我们枚举的状态为0时,后面的这一种语句是无法满足要求的,但在题目中不种植也算一种方案,所以我们就需要0,1互换这个操作
for(int i=;i<tot;i++) if(!(i&(i<<))) can[++cnt]=i;//所有左右两边不相邻的状态
这是保存状态的语句,tot=1<<n,n为列数.题目要求相邻两边不能同时种植,我们就把一个状态,左移一位也就是取它的下一位,再与它自己想与,若大于0,则代表有相邻的1,否则就没有.
这样就巧妙的判断了左右相邻的情况
for(int i=;i<=cnt;i++)if(!(cur[]&can[i])) dp[][can[i]]=;//预处理第1行的可行状态
只要预处理第1行就好了,后面的行数都是由它转移而来的对吧
for(int i=;i<m;i++) //枚举1~m-1行
for(int j=;j<=cnt;j++)//枚举所有可行的状态
if((cur[i]&can[j])==)//如果第i行满足环境要求
for(int k=;k<=cnt;k++)//枚举第i+1行的状态
if(((can[k]&cur[i+])==) && ((can[j]&can[k])==))//和第i+1行的状态满足第i+1行的环境以及不与的第i行状态冲突
dp[i+][can[k]]=(dp[i+][can[k]]+(dp[i][can[j]]%mod))%mod;//状态数相加
这就是本代码的核心程序,处理出每一行满足条件的方案数.
最后贴一下总代码
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstdio>
#include<string>
#define in(i) (i=read())
using namespace std;
const int mod=;
int read()
{
int ans=,f=; char i=getchar();
while(i<''||i>'') {if(i=='-') f=-; i=getchar();}
while(i>=''&&i<=''){ans=(ans<<)+(ans<<)+i-''; i=getchar();}
return ans*f;
}
int dp[][<<],can[<<],cur[];
int main()
{
int m,n,cnt=,ans=,tot;
in(m);in(n); tot=<<n;
//cur[i]表示第i行的环境
for(int i=;i<=m;i++) {
for(int j=;j<=n;j++) {
int a; in(a);//输入环境
if(!a) cur[i]|=(<<(n-j));//这个有两点要注意,一个是所有的0变成1,1变成0(这个必须),一个是反向(正向也可以)存环境------>cur[i]|=(1<<(j-1));
}
}
for(int i=;i<tot;i++) if(!(i&(i<<))) can[++cnt]=i;//所有左右两边不相邻的状态
for(int i=;i<=cnt;i++)if(!(cur[]&can[i])) dp[][can[i]]=;//预处理第1行的可行状态
for(int i=;i<m;i++)//枚举1~m-1行
for(int j=;j<=cnt;j++)//枚举所有可行的状态
if((cur[i]&can[j])==)//如果第i行满足环境要求
for(int k=;k<=cnt;k++)//枚举第i+1行的状态
if(((can[k]&cur[i+])==) && ((can[j]&can[k])==))//和第i+1行的状态满足第i+1行的环境以及不与的第i行状态冲突
dp[i+][can[k]]=(dp[i+][can[k]]+(dp[i][can[j]]%mod))%mod;//状态数相加
for(int i=;i<=cnt;i++)
ans=(ans+dp[m][can[i]])%mod;
cout<<ans<<endl;
return ;
}
状压dp的其他例题
2.Codefoces--Kefa and Dishes 题解
状态压缩十分有用,并不一定只能用于dp,有些范围比较大的数据结构有时也需要状压,留待同学们以后做题时自己去发现