poj 2411 Mondriaan's Dream

状态压缩DP

经典覆盖问题,输入n和m表示一个n*m的矩形,用1*2的方块进行覆盖,不能重叠,不能越出矩形边界,问完全覆盖完整个矩形有多少种不同的方案

其中n和m均为奇数的话,矩形面积就是奇数,可知是不可能完全覆盖的。接着我们来看n*m为偶数的情况

DP前先处理一下,交换n和m使n较大m较小,这样能减少状态数

另外数据中是有重复的,所以开辟一个ans数组来记录每组数据的结果,如果遇到相同的数据则不要计算直接输出答案

不用这个ans数组的话也不会超时,这个代码是跑出了950ms,加了这个记录答案的数组时间变为600ms

接着就看注释部分的讲解即可

/*

最上面的为第1行,最下面为第n行

从上到下按行DP

其中一行的状态我们用一个二进制表示,0表示没有被覆盖,1表示被覆盖了

最后得到一个01串,这个串变回十进制就是一个状态

定义状态dp[i][s],表示前i-1行已经放满,第i行的状态为s的方案数

状态转移方程为 dp[i][s]=sum{ dp[i-1][ss] } ,其中状态s与状态ss兼容

这个状态转移方程的内涵在于理解s和ss何为兼容

首先我们约定一个放置方法,就是竖着放的时候,我们暂且将其称为“上凸型摆放”

因为竖放必然占据第i-1行和第i行,我们约定这个方块是属于第i行的,也就是说它凸上去了

那么要在第i行的第j列竖放一个方块的话,第i-1行第j列必须没有方块

也就是说,第i行的放置是受到第i-1行的限制的,反过来说在第i行竖放了方块,也会影响第i-1行的状态

所以这样就可以讲解一下状态转移方程了,前i-2行已经放满了,第i-1行的状态为ss(dp[i-1][ss])

此时在第i行开始放一些方块,放的方法不定,可能横放可能竖放,但是按这个方案放完后

第i-1行刚好被填满,且第i行的状态变为了s,所以不难想到第i-1行的状态ss到第i行的状态s这个转移是唯一的

所以有 dp[i][s]=sum{ dp[i-1][ss] }

最后我们详细讨论一下s和ss在什么情况下是兼容的

1.第i行的第j列为1,第i-1行的第j列为1,这样的话,说明第i行的第j列一定不是竖放而是横放否则会与第i-1行的第j列冲突

  所以马上紧接着判断第i行第j+1列,如果是1,那么满足横放的规则,同时也要第i-1行第j+1列也要为1,否则的话这个格子没办法填充,

  成立后向左移动两格

  不满足上述条件的,就是两个不兼容或者不合法的状态

2.第i行第j列为1,第i-1行第j列为0,那么说明第i行第j列应该竖放并填充第i-1行第j列,成立后向左移动一格

3.第i行第j列为0,说明不放方块,那么第i-1行第j列必须为1,否则没法填充这个格子。若第i-1行第j列也为0,不兼容不合法

  (至于第i行第j列这个格子空着干什么,其实就是留出来给第i+1行竖放的时候插进来的)



那么目标状态是什么,就是dp[n][maxs],maxs表示全部是1的串,即第n-1行以上全部覆盖满,第n行的状态为maxs,即没有空着的格子,也全部覆盖满了

即整个矩形全部被覆盖满了的状态



最后是第1行的初始化问题,因为约定了“上凸型摆放”,所以第1行是不能竖放方格的,只能横放方格,

每横放一个必定占据两个格子,所以在判断一个状态(那个01串)的时候,连着的1的个数必定为偶数,如果出现了单独的1,说明不合法

*/



#include <cstdio>

#include <cstring>

#define N 15

#define MAX (1<<11)+10



long long dp[N][MAX];

long long ans[N][N];

int n,m;



bool init(int s)

{

    for(int k=0; k<m; )

    {

        if(s & (1<<k))

        {

            if(k==m-1) return false;

            if(s&(1<<(k+1))) k+=2;

            else return false;

        }

        else k++;

    }

    return true;

}



bool ok(int s, int ss)

{

    for(int j=0; j<m; )

        if(s & (1<<j)) //第i行第j列为1

        {

            if( ss & (1<<j)) //第i-1行第j列也为1,那么第i行必然是横放

            {

                //第i行和第i-1行的第j+1都必须是1,否则是非法的

                if( j==m-1 || !(s&1<<(j+1)) || !(ss&(1<<(j+1))) ) return false;

                else  j+=2;

            }

            else j++; //第i-1行第j列为0,说明第i行第j列是竖放

        }

        else //第i行第j列为0,那么第i-1行的第j列应该是已经填充了的

        {

            if(ss&(1<<j)) j++;//已经填充

            else return false;

        }

    

    return true;

}



void solve()

{

    int maxs;

    if(n<m)

    { n=n^m; m=n^m; n=n^m; }

    //交换后n是行m是列,m较小,那么状态数也可以相应减少

    maxs=(1<<m)-1;

    memset(dp,0,sizeof(dp));

    

    for(int s=0; s<=maxs; s++) //枚举第一行所有可能的状态

        if(init(s))

        {

            dp[1][s]=1; //方案数都是1

            //printf("%d\n",s);

        }



    for(int c=2; c<=n; c++) //按行dp

        for(int s=0; s<=maxs; s++) //第i行的状态

            for(int ss=0; ss<=maxs; ss++) //第i-1行的状态

                if(ok(s,ss))

                    dp[c][s] += dp[c-1][ss];

    

    

    printf("%lld\n",ans[n][m]=ans[m][n]=dp[n][maxs]);

}



int main()

{

    memset(ans,-1,sizeof(ans));

    while(scanf("%d%d",&n,&m)!=EOF)

    {

        if(!n && !m) break;

        if(!ans[n][m]) 

        {

            printf("%lld\n",ans[n][m]);

            continue;

        }

        if(n&1 && m&1) 

        {

            ans[n][m]=ans[m][n]=0;

            printf("0\n");

            continue;

        }

        solve();

    }

    return 0;

}

 

 

你可能感兴趣的:(poj)