POJ.2411. Mondriaan's Dream

这道题是压缩状态DP的经典题,经典到我百度这个题出了两三页的解题报告,但我看了前前后后快4个小时,还是不怎么明白,先把现在的思路写下。

题意的简单也奠定了这道题的经典性:给定一个m*n的面积,用2*1的瓷砖横贴或者纵贴铺满,不考虑对称性问有多少种铺法。在做这道题的时候第一次接触到“压缩状态DP”,按照我的理解,所谓的压缩状态DP就是把每种递推情况用一个更简洁(比如一个数)来表达,一个数推出另外的数,完成DP递推。有点像是康拓展开。

这道题就是这样做,以每一行为一个处理单位,设置一个dp数组dp[i][j],i的范围是行数height, j是一个二进制数转成的十进制表达,以01表示状态,j就是一个状态值,就是压缩后的状态,而这个元素值就是通过之前的行状态能够形成本行状态的最多铺法值。

横贴的两个砖都设为1,纵贴的上部分砖为0,下部分砖为1。也就是说,最后一行要全部为1才满足铺满的条件,也就是说最后输出的答案是dp[height][(1<<width)-1]的值。【(1<<width)-1是width个1】

好了,那现在一步步来,先求出状态转移方程。首先有个很重要的前提是:每一行的铺法都不是独立的,都是跟上一行相对应一起出现。因此我们可以这样想:要铺第i行瓷砖到某个状态path[j][1],那就必须累加其对应的i-1行的状态path[j][0],举个例子:如果我第i行想铺成1001的状态,那么就要把能形成这个状态的上一行状态(0110,1111)的方法数加起来;想要铺成(1111)的状态,就要把能形成(0000,1111,1100)的能方法数加起来。因此,状态方程为:

dp[i+1][path[j][1]]+=dp[i][path[j][0]];

这样子似乎会有很多浪费,比如我也会求出最后生成不符合条件的状态方法数,但是没办法只能这样推过去。

这个path[i][j]数组也是关键,是一个i的范围为11*(1<<width),即每一行的每一个状态数;j的范围是0或1,0表示当前行,1表示前一行;元素值为状态数。也就是说,每一行的每一个状态作为主行或者上一行的状态值,一组path[i][0]、path[i][1]是一种组合。这样的一个组合也是一个整题通用的数组。也就是说,对于特定的i状态,其上一行的状态就是path[i][1]。因此,在最后的dp数组中,把最后一行的情况扫出来就是答案了。

那么怎么求出这个path数组呢?有一种很神奇的方法:对于每块砖都可以选择三种处理方法:1)放横的2块;2)放纵的上块;3)放纵的下块。这里就是一个递归的思路了,因此要使用dfs,这个dfs对于两行进行一次横扫,每次扫都把这一行和下一行状态左移一位,然后在后面新增一位0或1,就像不断打补丁一样把整行的状态“打”出来。因此:

1)        本行要放一块横砖的话:(CurrRow<<2)|3,上一行也要放一块横砖:(PreRow<<2|3);这个因果关系也可以倒过来。

2)        本行要放一个纵块的上砖时(即该位置为0),那么上一行就要放一个1。

3)        本行要放一个纵块的下砖时(即该位置为1),那么上一行就要放一个0.

通过这样进行一次dfs直到扫完了整行之后,就可以得到所有符合条件的两行之间的状态数。

因此最后就通过这样的递推一行行把结果推出来就可以了。


感谢博主:http://blog.csdn.net/shiwei408/article/details/8821853 以及其他写了这道题的博主,我是主要参考了这篇博文写出来的,谢谢!


/*
 * POJ2411Mondriaan'sDream.cpp
 *
 *  Created on: 2014年7月7日
 *      Author: Prophet
 */
#include<stdio.h>
#include<string.h>
void dfs(int column,int CurrRow,int PreRow);
long long dp[13][2100];//1<<11,第一个参数表示第几行,第二个参数表示状态,值等于最大值
int path[14000][2];//11*(1<<11),每一行对应本行的状态(0),以及上一行的状态(1)
int times;
int height,width;
int main(){
	while(scanf("%d%d",&height,&width)){
		if(height==0&&width==0)
			break;
		else if((height*width)%2==1)
			printf("0\n");
		else{
			if(height<width){//保证高度一定大于宽度
				int temp=width;
				width = height;
				height = temp;
			}
			times = 0;
			dfs(0,0,0);
			memset(dp,0,sizeof(dp));
			dp[0][(1<<width)-1]=1;//第一行全为1的拼法有1种
			for(int i=0;i<height;i++){
				for(int j=0;j<times;j++){
					dp[i+1][path[j][1]]+=dp[i][path[j][0]];
				}
			}
			printf("%lld\n",dp[height][(1<<width)-1]);
		}
	}
	return 0;
}

void dfs(int column,int CurrRow,int PreRow){
	if(column>width)
		return;
	else if(column==width){
		path[times][0]=PreRow;
		path[times][1]=CurrRow;
		times++;
		return;
	}
	else{
		dfs(column+2,(CurrRow<<2)|3,(PreRow<<2|3));//放横的两块砖
		dfs(column+1,(CurrRow<<1)|1,PreRow<<1);//放纵的下砖
		dfs(column+1,CurrRow<<1,(PreRow<<1)|1);//放纵的上砖
	}
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值