【POJ2411】Mondriaan's Dream-状态压缩DP(插头DP?)

测试地址:Mondriaan's Dream

题目大意:求用1*2的骨牌完美覆盖h*w的棋盘的方法数。

做法:这道题绝对是经典题啊,久仰大名......关于轮廓线和插头的思想可以看cdq大大的论文:点这里。但这题不用搞那么麻烦,因为骨牌之间相互独立,所以不用考虑插头的连通性问题,直接无插头为0,有插头为1状态压缩即可。

首先如果h*w为奇数,直接输出0。对于其他情况,用f[i][j][state]表示骨牌覆盖前i-1行和第i行的前j列,且轮廓线状态为state的方案数,其中state是一个w+1位的二进制数。这里如果状态中一位为1,就表示这个位置有一个格子在等待着某一个状态的另一个格子去接上,从而形成一块骨牌,要注意的是轮廓线的弯折部分相邻两个插头不能同为1,因为我们知道一个格子不可能同时有两个位置的格子去接,一个格子也不可能同时接上两个格子。

接下来考虑逐格递推,当递推到第i行第j列的格子时,考虑它是去接上一个格子从而拼成一块骨牌,还是新开一个格子等待接下来的格子来接。枚举状态,这时观察轮廓线的外凸部分(想象一下,即当前格子的右边线和下边线),如果为“00”,则表示这个格子是去接上一个前面的格子,否则表示这个格子是新开的。对于接前面格子的情况,考虑什么样的状态才能推到当前状态,答案就是去掉当前格的轮廓线上,内凹部分(即当前格的左边线和上边线)某一个插头是1,而其他插头都不变的状态。这样只有两个状态能推到当前状态,累加即可。对于新开格子的情况,考虑什么样的状态才能推到当前状态,答案是去掉当前格的轮廓线上,内凹部分插头都是0,其他插头都不变的状态。这样只有一个状态能推到当前状态,累加即可。最后对于每一行最后一列格子的状态转移特殊处理即可。可以知道,最后的答案就是f[h][w][0]。

这样整个DP的时间和空间复杂度都是O(n^2*2^n),鉴于n最大只达到11,所以完全可行。如果你觉得空间复杂度太高,可以用滚动数组压到O(2^n)。

以下是本人代码(初次写,写得很丑,见谅......):

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
int n,m,now,past;
ll f[2][3010],s[3010];

int main()
{
  while(scanf("%d%d",&n,&m)&&n&&m)
  {
    if (n*m%2!=0) {printf("0\n");continue;}
	memset(f,0,sizeof(f));
	f[0][0]=1;
	now=1,past=0;
	if (n<m) swap(n,m);
	for(int i=1;i<=n;i++)
	  for(int j=1;j<=m;j++)
	  {
	    memset(f[now],0,sizeof(f[now]));
	    for(int k=0;k<(1<<(m+1));k++)
		{
		  int bit0,bit1,bit2;
		  bit0=k&(1<<(j-1));
		  bit1=k&(1<<j);
		  bit2=k&(1<<(j+1));
		  if ((bit0&&bit1)||(bit1&&bit2)) continue;
		  if (!bit0&&!bit1)
		  {
		    f[now][k]+=f[past][k+(1<<(j-1))];
			f[now][k]+=f[past][k+(1<<j)];
		  }
		  else
		  {
		    if (bit0) f[now][k]+=f[past][k-(1<<(j-1))];
		    if (bit1) f[now][k]+=f[past][k-(1<<j)];
		  }
		}
		if (j==m)
		{
		  memset(s,0,sizeof(s));
		  for(int k=0;k<(1<<m);k++)
		    s[k<<1]=f[now][k];
		  for(int k=0;k<(1<<(m+1));k++)
		    f[now][k]=s[k];
		}
		swap(now,past);
	  }
	printf("%lld\n",f[past][0]);
  }
  
  return 0;
}


posted @ 2017-03-20 17:47  Maxwei_wzj  阅读(106)  评论(0编辑  收藏  举报