在了解状压DP之前,首先要了解简单的位运算。
简单的位运算
类型 | 符号 | 规则 | 例子 |
---|---|---|---|
与 | & | 同1为1,其余为0 | 11&3= 00001011&00000011 = 00000011 = 3 |
向左位移 | << | 向左移一位,右边自动补0 | 11<<1 = 00001011<<1 = 00010110 = 22 |
向右位移 | _>> | 向右移一位,左边自动补1 | 11>>1 = 00001011>>1 = 00000101 = 5 |
我们先看一道入门题来讲解状压DP。
例题-玉米地
题目描述
John租了一片矩形农场,农场由M(1 <= M <= 12)行N(1 <= N<= 12)列的方格构成。有的方格土地肥沃,可以种植庄稼;有的方格土地贫瘠,则不可以种植。同时,为了让作物长得好,一个种了作物的方格相邻的(上下左右共四个)方格不可以种植。他希望知道一共由多少种不同的种植方案。
输入
第一行是两个数M和N
接下来的M行每行N个数,每个数代表当前位置是否可以种植,1表示可以,0表示不可以。
输出
输出一行,包含一个整数,代表总的种植方案的个数对100000000取模后的值。
样例输入
2 3
1 1 1
0 1 0
样例输出
9
题目解析
状压DP全称状态压缩DP,指的是根据题目情况和数据范围压缩数组的维度。对于本题,常见的方法是将每一行的方格肥沃状态以及作物可能的种植状态用二进制表达,再压缩为十进制。
以样例输入举个例子:
1 1 1
0 1 0
第一行
肥沃状态: 111->7
作物可种植状态:
000->0(都不种)
001->1
010->2
100->4
101->5
第二行
肥沃状态: 010->2
作物可种植状态:
000->0(都不种)
010->2
那么如何将两行合并在一起呢?
设dp[i][j]代表当第i行取j状态时,种植作物的最大值。对于第一行,可取的状态有0,1,2,4,5;对于第二行,可取的状态有0,2。我们对于这些可取的状态进行枚举计算是否矛盾。
首先第一行是0,第二行也是0。解压后可得
000
000
很明显在这种状态下,并没有作物是相邻的。
然后第一行还是0,第二行取2。解压后可得
000
010
也没有作物相邻。
直到第一行是2,第二行也是2。解压后发现
010
010
第二列的作物相邻了!
位运算此时可以大显神威了:设两个状态没有相邻作物为0,否则为1。在与运算(&)中,同1为1,其余为0。000&000=0,000&010=0,010&010=1。因此状态转移方程为:dp[i][j]+=dp[i-1][k]。这里的k代表不与状态j矛盾的状态。如何判断两个状态是否矛盾前面已经提到过了。
下面是代码
题目代码
//cur[i]代表第i行方格肥沃状态
//state储存本身合法的状态,暂时不考虑方格肥沃状态
//如000,010等合法;110,011等由于作物已经相邻不合法
//total代表状态的总数量
//top记录本身合法状态的总数量
//dp已经提到过
#include<iostream>
#include<cstdio>
#define mod 100000000
using namespace std;
int state[600],cur[13],top=0;
int dp[13][600];
int n,m;
void init()//初始化state
{
int total=(1<<n);//这里用到了左移位运算
//意思是将1左移n位,也就是二进制中1后n个0。转化为十进制可以看作2的n次方
for(int i=0;i<total;i++)
if(!(i&(i<<1)))
state[++top]=i;//记录枚举的合法状态
}
int main()
{
int flag;
scanf("%d%d",&m,&n);
init();
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
scanf("%d",&flag);
if(!flag)cur[i]+=(1<<(n-j)); //这里用到了左移位运算记录肥沃状态
//意思是如果可种植,加上1左移n-j位 。请读者自行思考:为什么?我相信不难理解
}
for(int i=1;i<=top;i++)
if(!(state[i]&cur[1]))dp[1][i]=1;//初始化dp
for(int i=1;i<=m;i++)//三重循环 首先枚举行
for(int j=1;j<=top;j++)//然后枚举本身合法状态
{
if(state[j]&cur[i])continue;//如果该状态j不适用于第i行肥沃状态则枚举下一个,否则继续
//这里用到与运算&,如果两个状态当中有两个相邻都是1,那么得到结果是真
//若得到真,则枚举下一个
for(int k=1;k<=top;k++)//然后再一次枚举本身合法状态
{
if(state[k]&cur[i-1])continue;//如果该状态k适用于第i-1行肥沃状态则枚举下一个,否则继续
if(state[j]&state[k])continue;//如果状态j和k矛盾则枚举下一个,否则
dp[i][j]=(dp[i][j]+dp[i-1][k])%mod;//终于千辛万苦到达状态转移方程
}
}
int ans=0;
for(int i=1;i<=top;i++)
ans=(ans+dp[m][i])%mod;//不解释了,都应该看得懂
cout<<ans<<endl;
return 0;
}