状压DP(涉及位运算)

转自:

https://blog.csdn.net/forever_dreams/article/details/81074971

【简介】

状压DP是基于状态压缩动态规划,又叫做集合动态规划。

顾名思义,这是一类以集合为状态的特殊的动态规划问题。有些时候,需要被记录到得状态有很多,但是对每个状态都开一维来记录显然是行不通的,我们就考虑把这些状态压缩一下,通常情况下,若只有两种状态,我们会把状态用二进制表示,然后把它转成十进制来记录

基于状态压缩的动态规划是一种以集合内的元素作为状态,状态总数为指数级别的动态规划,它有以下两个特点:

  1. 具备动态规划的两个基本性质,即最优化原理(最优子结构性质)无后效性
  2. 数据规模的某一维或者几维非常小

 

【位运算】

状压DP会经常用到位运算,我简单介绍一下

链接位运算(百度上的讲解)

位运算会把两个数转成二进制后再计算,计算时是按位处理,即结果单独计算出来,再组成一个新数

  • 与运算&(and)相同位都为1,则为1;若有一个不为1,则为0。 即0&0=0,0&1=0,1&0=0,1&1=1
  • 或运算|(or)相同位只要一个为1即为1。 即0|0=0,0|1=1,1|0=1,1|1=1
  • 异或运算^(xor)相同位不同则为1,相同则为0。 即0^0=0,0^1=1,1^0=1,1^1=0
  • 取反操作~(not)把0和1全部取反。 即每位0变1,1变0,要注意一下它会将有符号整数的符号位也取反
  • 左移操作<<(shl)a << b 是把二进制数a向左(高位)移b位(低位的最后b位补0)。
  • 右移操作>>(shr)a >> b 是把二进制数a向右(低位)移b位(高位的起始b位补0)。

不难发现,若对于十进制数 x,y,那么x << yx\times 2^{y}x >> y = x\div 2^{y},可以根据这个干些神奇的事情

上面的东西是基础知识,下面介绍一些常用的操作(可以自己推一下,也不难理解)

  • 取出二进制数 x 的第 k 位,存在 y 中:y = 1 << k & x(或 y = x >> k & 1)
  • 取出二进制数 x 的最后一个 1(也就是 lowbit),存在 y 中:y = x & -x
  • 将二进制数 x 的第 k 位变为 1:x = x | 1 << k
  • 将二进制数 x 的第 k 位变为 0:x = x & ~(1 << k)
  • 将二进制数 x 的第 k 位取反:x = x ^ 1 << k
  • 取出二进制数 x 的最后 k 位,存在 y 中:y = x & (1 << k)- 1
  • 将二进制数 x 低位连续的 1 变为 0:x = x & x + 1
  • 将二进制数 x 低位连续的 0 变为 1:x = x | x - 1
  • 取出二进制数 x 低位连续的 1,存在 y 中:y =(x ^ x + 1)>> 1
  • 快速枚举集合 S(用二进制数 x 表示)的所有子集:for(i = x;i ! = 0;i =(i - 1)& x)

下面是各个操作的优先级(同级的是从左到右运算)

  1.      ( )     [ ]     ->     .     ::      ++(后置)     --(后置)
  2.      !     ~     ++(前置)     --(前置)     -     +     *     &     ( type )     sizeof
  3.      ->*     .*
  4.      *     /     %
  5.      +     -
  6.      <<     >>
  7.      <     <=     >     >=
  8.      ==     !=
  9.      &
  10.      ^
  11.      |
  12.      &&
  13.      ||
  14.      ? :(三元运算符)
  15.      =     +=     -=     *=     /=     %=     &=     ^=     |=     <<=     >>=
  16.      ,

【例题+讲解】

题目大意:有一个W行H列的广场,需要用1*2小砖铺盖,小砖之间互相不能重叠,能不能铺完整个广场?若能,输出有多少种不同的铺法;若不能,输出 -1(1≤W,H≤11)

对于这道题,用暴搜的话应该是要超时的(我也不太清楚,没试过)

首先我们考虑,若W和H都是奇数,那肯定是铺不成的(因为此时广场的面积为奇数,而小砖的面积为偶数)

剩下的情况,我们就用状压DP来做

我们用 f [ i ][ j ] 表示铺到第 i 行,状态为 j 的方案数。在DP的时候用 i 表示当前行s1 表示当前行的状态s2 表示下一行的状态d 表示到了 d 这个点,DP时具体操作如下:

  • 如果点 d 没有被覆盖,那就可以竖放,并把 d 下面那个点记为覆盖了(即改变s2)
  • 如果点 d 和点 d + 1 都没有被覆盖,那就可以横放,不用改s2
  • 如果点 d 被覆盖了,直接到下一个点 d + 1

(自己画图更好理解一点)

初始化:f [ 1 ][ 0 ] = 1(即第一行没有铺过砖的方案数,显然为 1)

这样的话,最后的答案answer = f [ H + 1 ][ 0 ](即第 H + 1 行没有铺过砖的方案数,显然就是答案)

最终的时间复杂度为O(H\cdot 2^{W}\cdot 2^{W},即O(H\cdot 2^{2W},这样的话,我们有一个小优化,当W>H的时候,就交换W和H,相当于把矩形翻转一下,但这样会以小的数为指数,会优化一点

代码(代码中的神奇位运算操作可以去上面看,应该都有提到):

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=15,M=1<<11;
int w,h,state;
long long f[N][M];
void dp(int i,int s1,int s2,int d)
{
	if(d==w)
	  f[i+1][s2]+=f[i][s1];  //如果这一行已经走完,直接累加答案 
	else
	{
		if(!(s1&(1<<d)))     //如果第i行的第d位没有覆盖过 
		{
			dp(i,s1,s2|(1<<d),d+1);    //将砖竖放,并把d下一行的相应位置标记为覆盖过,再dp下去 
			if(d+2<=w&&!(s1&(1<<(d+1))))
			  dp(i,s1,s2,d+2);         //将砖横放,直接把d移到d+2的位置 
		}
		else
		  dp(i,s1,s2&~(1<<d),d+1);     //如果第i行的第d位被覆盖过,就直接到第d+1位,其中可以不用s2&~(1<<d),直接用s2也行 
	}
}
int main()
{
	int i,j;
	scanf("%d%d",&w,&h);
	if(w*h&1)      //奇数&1为1,偶数&1为0 
	{
		printf("-1");
		return 0;
	}
	if(w>h)  swap(w,h);      //小优化 
	state=1<<w;    //状态总数 
	f[1][0]=1;     //初始化 
	dp(1,0,0,0);   //先处理出从第一行到第二行的值 
	for(i=2;i<=h;++i)        //枚举每一行 
	  for(j=0;j<state;++j)   //枚举每一种状态 
	    if(f[i][j])          //这里的意思是,如果连f[i][j]都没有更新到,那以它出发dp到的也不会更新,就不用dp了 
	      dp(i,j,0,0);       //状压DP 
	printf("%lld",f[h+1][0]);
	return 0;
}

 

【入门题目推荐】

状压DP和普通DP一样,都需要大量刷题来找感觉,这里推荐几道入门题,感兴趣的话可以做一下

POJ 3254     Corn Fields

POJ 1185     炮兵阵地

洛谷 1171     售货员的难题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值