态压缩动态规划(简称状压dp)是另一类非常典型的动态规划,通常使用在NP问题的小规模求解中,虽然是指数级别的复杂度,但速度比搜索快,其思想非常值得借鉴。
为了更好的理解状压dp,首先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3.’^’符号,x^y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。
这四种运算在状压dp中有着广泛的应用,常见的应用如下:
1.判断一个数字x二进制下第i位是不是等于1。
方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)
将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x = x | ( 1<<(i-1) )
证明方法与1类似,此处不再重复证明。
3.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x-1)
感兴趣的读者可以自行证明。
位运算在状压dp中用途十分广泛。
1.(k&-k)在状态压缩和树状数组中都经常能够看到,那么(k&-k)的值是什么含义呢?
这个值是把k的二进制的高位1全部清空,只留下最低位的1,当然如果只有一位1,则保留等于k本身。该操作就是留下k二进制数中最低位的一个1
2.两种相等的形式:i-(i&-i) = i^(i&-i)
他们都表示减去最低位的1,再返回值
3.状态压缩的方法,就是利用二进制数的零一进行模拟。零代表没取,一代表取了。时间复杂度O(2^n)只能用在n小于20的情况下。首先在存储每个元素的时候就按照(1,10,100,1000)的方法来存储,需要枚举所有的子集时可以利用sum[i] = sum[i-(i&-i)] + a[i & -i] 每个sum[i],都表示i在二进制中1取零不取。可以进行初始化,算出每个i的二进制一的个数。
//bc[i]表示i的二进制表示中一的个数是多少
bc[0] = 0;
for (i=1; i<(1<<20); i++)
bc[i] = bc[i-(i&-i)] + 1;
for (i=0; i<m; i++) scanf("%d", &tmp[1<<i]);//tmp存储的时候就是按照1左移存储的
sum[0] = 0;
for (i=1; i<(1<<m); i++)
sum[i] = sum[i-(i&-i)] + tmp[i&-i];/*每次都保证减去最低位后的状态是已经求出的,或者状态是空sum[0]。比如从1000开始到1001(1000求出)到1010(1000求出)到1011(减去最低位后为1010也已求出)。那么为什么地位减去后一定是已求出的呢,因为减去小于其本身,sum[i]又是顺序遍历的
此处还有问题,为什么是不重不漏的呢?每次到临界值(1,10,100,1000)时sum[i]的值都等于tmp[i].当减去最低位后,不肯能是等于它本身的,此时又再加上了另一个数,
注意此处的1,0是二进制。实际上是sum[1] = tmp[1],sum[10] = tmp[10],sum[11] = sum[10] + temp[1] = temp[10] + temp[1],以此类推sum[1111] = temp[1] + temp[10] + temp[100] + temp[1000]
其实很简单的啦,每次sum[i]表示的都是对于i的二进制数来说0,代表取到1代表取不到。肯定是全部的状态(不包括都取不到的,那种是sum[0]的情况)*/
5.一定要注意的问题:
原数组,存储所有子集的数组,还有用来存储任意个数二进制个数的数组大小都要开到2^n次方。另,当n <= 20时,时间空间都可以容纳2^20
【POJ3254】Corn Fields
【在线测试提交传送门】
【问题描述】
农夫有一块地,被划分为m行n列 N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) 大小相等的格子,其中一些格子是可以放牧的(用1标记),农夫可以在这些格子里放牛,其他格子则不能放牛(用0标记),并且要求不可以使相邻格子都有牛。现在输入数据给出这块地的大小及可否放牧的情况,求该农夫有多少种放牧方案可以选择(注意:任何格子都不放也是一种选择,不要忘记考虑!)
【输入格式】
第一行,两个整数,表示M和N。 第2到第M+1行,每行N个整数,1表示可以放牧,0表示不可以放牧。
【输出格式】
一行,一个整数,表示能放牧方案数,将结果对100,000,000取模。
【输入样例1】
2 3 1 1 1 0 1 0
【输出样例1】
9
【解析】根据题意,把每一行的状态用二进制的数表示,0代表不在这块放牛,1表示在这一块放牛。首先很容易看到,每一行的状态要符合牧场的硬件条件,即牛必须放在能放牧的方格上。这样就能排除一些状态。另外,牛与牛之间不能相邻,这样就要求每一行中不能存在两个相邻的1,这样也能排除很多状态。然后就是根据上一行的状态转移到当前行的状态的问题了。必须符合不能有两个1在同一列(两只牛也不能竖着相邻)的条件。这样也能去掉一些状态。然后,上一行的所有符合条件的状态的总的方案数就是当前行该状态的方案数。
【状态表示】dp[state][i]:在状态为state时,到第i行符合条件的可以放牛的方案数
【状态转移方程】dp[state][i] =Sigma dp[state’][i-1] (state’为符合条件的所有状态)
【DP边界条件】首行放牛的方案数dp[state][1] =1(state符合条件) OR 0 (state不符合条件)
#include <cstdio>
#include <cstring>
using namespace std;
#define mod 100000000
int M,N,top = 0;
int state[600],num[110];
int dp[20][600];
int cur[20];
inline bool ok(int x){
if(x&x<<1)return 0;
return 1;
}
void init(){
top = 0;
int total = 1 << N;
for(int i = 0; i < total; ++i){
if(ok(i))state[++top] = i;
}
}
inline bool fit(int x,int k){
if(x&cur[k])return 0;
return 1;
}
inline int jcount(int x)
{
int cnt=0;
while(x)
{
cnt++;
x&=(x-1);
}
return cnt;
}
int main(){
while(scanf("%d%d",&M,&N)!= EOF){
init();
memset(dp,0,sizeof(dp));
for(int i = 1; i <= M; ++i){
cur[i] = 0;
int num;
for(int j = 1; j <= N; ++j){
scanf("%d",&num);
if(num == 0)cur[i] +=(1<<(N-j));
}
}
for(int i = 1;i <= top;i++){
if(fit(state[i],1)){
dp[1][i] = 1;
}
}
for(int i = 2; i <= M; ++i){
for(int k = 1; k <= top; ++k){
if(!fit(state[k],i))continue;
for(int j = 1; j <= top ;++j){
if(!fit(state[j],i-1))continue;
if(state[k]&state[j])continue;
dp[i][k] = (dp[i][k] +dp[i-1][j])%mod;
}
}
}
int ans = 0;
for(int i = 1; i <= top; ++i){
ans = (ans + dp[M][i])%mod;
}
printf("%d\n",ans);
}
}
【POJ1185】炮兵阵地–经典状压DP
【在线测试提交传送门】
【问题描述】
司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队。一个N*M的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用"P"表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示。
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。 现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
【输入格式】
第一行包含两个由空格分割开的正整数,分别表示N和M; 接下来的N行,每一行含有连续的M个字符('P'或者'H'),中间没有空格。按顺序表示地图中每一行的数据。N ≤ 100;M ≤ 10。
【输出格式】
仅一行,包含一个整数K,表示最多能摆放的炮兵部队的数量。
【输入样例】
5 4 PHPP PPHH PPPP PHPP PHHP
【输出样例】
6
【解析】可以发现,对于每一行放大炮的状态,只与它上面一行和上上一行的状态有关,每一行用状态压缩的表示方法,0表示不放大炮,1表示放大炮,同样的,先要满足硬件条件,即有的地方不能放大炮,然后就是每一行中不能有两个1的距离小于2(保证横着不互相攻击),这些要预先处理一下。然后就是状态表示和转移的问题了,因为是和前两行的状态有关,所以要开个三维的数组来表示状态,当前行的状态可由前两行的状态转移而来。即如果当前行的状态符合前两行的约束条件(不和前两行的大炮互相攻击),则当前行的最大值就是上一个状态的值加上当前状态中1的个数(当前行放大炮的个数)
【状态表示】dp[i][j][k] 表示第i行状态为k,第i-1状态为j时的最大炮兵个数。
【状态转移方程】dp[i][k][t] =max(dp[i][k][t],dp[i-1][j][k]+num[t]); num[t]为t状态中1的个数
【DP边界条件】dp[1][1][i] =num[i] 状态i能够满足第一行的硬件条件(注意:这里的i指的是第i个状态,不是一个二进制数,开一个数组保存二进制状态)
#include <cstdio>
#include <cstring>
using