POJ2411 Mondriaan's Dream (轮廓线动态规划典型例题 以及用状态压缩逐行深搜推的方法)

来源:互联网 发布:云计算前景分析 编辑:程序博客网 时间:2024/05/26 12:59

题目链接:http://poj.org/problem?id=2411

参考博客:http://blog.csdn.net/u013480600/article/details/19499899

给出一个n*m的矩形,然后用1*2大小的多米若骨牌去填充n*m的这个矩形,问有多少种填充方法。

分析:典型的轮廓线动态规划题目。

首先本题目是以一个一个的格子为基础来计算状态的,即每次都是考虑当前位置的格子如何放左上骨牌(以当前位置为最右下角,即只不放,左放,和上放3种情况,没有右放和下放)。且本题的状态都是一条一条的轮廓线。如图:

1

1

1

1

1

1

1

K4

K3

K2

K1

K0

O

 

 

 

 

 

 

 

令<K3K2K1K0O> 为这5个数字对应的二进制值的十进制结果。K3K2K1K0O=11110表示由这5个格构成的轮廓线状态 如


K4 K3 K2 K1 K0 O 分别为: 0 1 1 1 1 0 时,

<K3K2K1K0O> = 30,<K4K3K2K1K0> = 15


本题的状态为d[cur][<K3K2K1K0O>]表示 当前以O格为末尾的轮廓线状态为<K3K2K1K0O>的值对应二进制表示时且K3之前的所有格子的值都为1时构成上图所给状态的方法总数。其中 当前轮廓线为K3K2K1K0O,前一个轮廓线为K4K3K2K1K0。当前的轮廓线状态数用d[cur][ <K3K2K1K0O>]表示,前一个轮廓线状态数用d[1-cur][ <K4K3K2K1K0>]表示。


现在初始时 K4K3K2K1K0O的值对应为 011110.

O格的每一种操作(不放,左方,上方)都会联系前后两种不同的轮廓线。

假设在O格放上骨牌,K4和O值都变为1,则d[cur][<K3K2K1K0O>] =d[cur][<11111>]  +=d[1-cur][<K4K3K2K1K0>]=d[1-cur][<01111>] 这句话的意义是 在O格放上骨牌,则以O格为末尾的轮廓线为11111,且已放的格子情况为:

1

1

1

1

1

1

1

1

1

1

1

1

1

 

 

 

 

 

 

 

 

 

 

 

 

的时候有d[1-cur][<01111>](这是一个整数值)这么多种情况是通过在轮廓线<K4K3K2K1K0>=<01111>的基础上,放O格上骨牌而产生来的。

假设在O格不放,K4的值不改变依然为0,如果执行:

d[cur][<K3K2K1K0O>] =d[cur][<11110>]  +=d[1-cur][<K4K3K2K1K0>]=d[1-cur][<01111>] 这样是非法的。因为不放O格,K4的值永远为0(K4的值如果为0只能通过O格放上骨牌来置1,O格之后的格子无论怎么放骨牌都不会改变K4的值),所以如果执行上述非法操作,违反了关于d[cur][<K3K2K1K0O>]的定义(要求K3之前所有格子的值都为1).

 

假设在O格放左骨牌,也是非法操作,因为O格左边已经是1了,不可能放的下左骨牌。

且在首行不能上放,在首列不能左放。

 

最后当循环处理到第(n,m)格时,轮廓线为<11111>时表示轮廓线前面的n-1行也全是11111,所以最终结果为d[cur][<11111>]。

并且初始值为d[0][<11111>]=1,其他d[0][其他值]=0(其实这个可以不弄,因为首行不能上放,所以首行轮廓线状态数不可能会引用到d[0][01111]这样的初值。)。

 

AC代码:

#include <iostream>#include <cstdio>#include <cstring>#include <string>#include <algorithm>#include <cmath>using namespace std;#define maxn 15int n,m,cur;long long dp[2][1<<maxn];//存就状态和新状态void update(int a,int b)//a是包含m位二进制数的老状态,b是包含m+1位二进制数的新状态{if(b&(1<<m))////判断新轮廓线首位 只有新轮廓线首位为1时才更新dp[cur][b^(1<<m)]+=dp[1-cur][a];//b^(1<<m)是将b状态的首位变为0 转化成包含m为的状态}int main(){while(~scanf("%d%d",&n,&m)&&n&&m){memset(dp,0,sizeof dp);cur=0;dp[cur][(1<<m)-1]=1;//状态1表示该点已经被放置 0表示为被放置//这步的目的上让第一行不能让上矩形for(int i=0;i<n;i++)for(int j=0;j<m;j++){cur=1-cur;memset(dp[cur],0,sizeof dp[cur]);for(int k=0;k<(1<<m);k++)//k的二进制形式表示前一个格子的轮廓线状态{update(k,k<<1);//当前格不放,直接k左移一位就表示带m+1位的新轮廓线的状态if(i&&!(k&(1<<(m-1))))//判断是否为第一行  判断旧轮廓线首位是否为0,为0才可以上方update(k,(k<<1)^(1<<m)^1);//因为要上放,所以对新轮廓线的首尾均更新为1if(j&&!(k&1))//判断是否为第一列  判断旧轮廓线尾位是否为0,为0才可以往左放update(k,(k<<1)^3);//因为要左放,所以对新轮廓线的末尾两位更新为1}}printf("%lld\n",dp[cur][(1<<m)-1]);//当最后一行的状态均为1时 满足整个矩阵都已被放满}return 0;}

假设第一行已经填满,则第二的摆设方式,只与第一对第二的影响有关。同理,第三的摆设方式也只与第二对它的影响有关。那么,使用一个长度为N的二进制数state来表示这个影响,例如:4(00100)就表示了图上第二的状态。

因此,本题的状态可以这样表示:

dp[i][state]表示该填充第i,第i-1对它的影响是state的时候的方法数。i<=M,0<=state<2N

对于每一,情况数也有很多,但由于N很小,所以可以采取搜索的办法去处理。对于每一,搜索所有可能的放木块的情况,并记录它对下一的影响,之后更新状态。状态转移方程如下:

dp[i][state]=∑dp[i-1][pre]每一个pre可以通过填放成为state

对于每一列的深度优先搜索

AC代码如下:

#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<queue>using namespace std;#define maxn 12int n,m;long long  dp[maxn][1<<maxn];void dfs(int i,int j,int old,int nex){if(j>m){dp[i+1][nex]+=dp[i][old];return ;}if(1<<(j-1)&old)dfs(i,j+1,old,nex);if((1<<(j-1)&old)==0)dfs(i,j+1,old,nex|1<<(j-1));if (j+1<=m && ((1<<(j-1))&old)==0 && ((1<<(j))&old)==0)        dfs(i,j+2,old,nex);return ;}int main(){while(~scanf("%d%d",&n,&m)&&n&&m){memset(dp,0,sizeof dp);dp[0][0]=1;for(int i=0;i<n;i++){for(int j=0;j<(1<<m);j++){if(dp[i][j])dfs(i,1,j,0);}}printf("%lld\n",dp[n][0]);}    return 0;}