状态压缩DP例题:NOJ:1413:Mondriaan's Dream

Mondriaan’s Dream

总时间限制:
3000ms
内存限制:
65536kB

描述
Squares and rectangles fascinated the famous Dutch painter Piet Mondriaan. One night, after producing the drawings in his ‘toilet series’ (where he had to use his toilet paper to draw on, for all of his paper was filled with squares and rectangles
在这里插入图片描述
Expert as he was in this material, he saw at a glance that he’ll need a computer to calculate the number of ways to fill the large rectangle whose dimensions were integer values, as well. Help him, so that his dream won’t turn into a nightmare!

输入
The input contains several test cases. Each test case is made up of two integer numbers: the height h and the width w of the large rectangle. Input is terminated by h=w=0. Otherwise, 1<=h,w<=11.

输出
For each test case, output the number of different ways the given rectangle can be filled with small rectangles of size 2 times 1. Assume the given large rectangle is oriented, i.e. count symmetrical tilings multiple times.
在这里插入图片描述
样例输入
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

样例输出
1
0
1
2
3
5
144
51205

首先来解析一下题目,题意就是,给出一个大矩形的行数和列数,问有多少种情况可以只用一个2×1的小矩形去没有相互覆盖地填满整个大矩形。那么分析过后很容易得出一类特殊情况,也就是行数和列数都为奇数的时候,那么格子数必定是奇数,但是小矩形至少都得占2个,不管多少个小矩形组成的大矩形,大矩形的格子总数肯定是偶数,所以只要行数列数全为奇数就可直接输出0了。

那么显然还有更多复杂的情况需要考虑,此时就要用到状态压缩dp。如果不懂这个概念也不要紧,理解思路也就慢慢可以了解这种方法了,但是首先有一个东西一定得了解,就是位运算,这里就不展开讲解了。首先我们考虑状态表示的问题,也就是我们如何用代码表示一个现有的情况呢,那么首先我们可以得到小矩形的摆放大体上看其实就只有两种情况:横着放,竖着放。但是其实对于每个小方格来说呢,有三种情况:
(1)横着放的两块。(2)竖着放的上面一块。(3)竖着放的第二块。

这里可能会有点奇怪,为什么竖着的要分开两种情况呢,因为我在此选用的是最终要进行按行进行动态规划,所以要考虑此方格是否对下面的行有影响。那么显然,会对下面有所影响的其实只有竖着放的上面一个。所以我们需要把竖着的第一个再分出一种情况。那么此时我们对状态赋予数值:
(1)横着放的一整块,两个小方格上都是1。
(2)竖着放的上面一块数值为0。
(3)竖着放的下面一块数值为1。
也就是将不会对下行有影响的全部为1,否则为0。因此我们也可以知道,我们的最后一行一定要全是1,因为不可能对下一行再影响了。

我们那我们现在规定了状态的数值,我们先姑且不考虑正确性,一行中一共可能有多少种情况呢,显然是2m种,把每行都看成二进制一样,就只有两个数值,一行m个位置,自然就是2m种情况了,至于正确性那就要我们自己在代码里面去讨论了。
然后我们再去表示完整状态:首先一点,我们一行中,其状态用一个数字就可以代替了,因为依据上面的分析,每行的每种情况无论对错都是一串二进制码,那么每种情况一一对应的数值的范围就是0~2m-1了。那么现在给出设计的dp含义,dp[ i ][ j ]就是代表,在第i行,第j种的情况下(也就是摆放的状态的二进制对应数字为j的),与i-1行的所有正确情况可以匹配成功的总情况数。也就是dp[ i ][ j ]+=dp[ i-1 ][ k ](0<=k<=2m-1),因为只要我此行的第j种情况与i-1行的k情况没有发生冲突并且自己单行来看本身是正确的话,那么我就可以加上dp[ i ][ k ]上所留有的所有情况数。那么到最后,我们前面也分析了,只有全部为1的情况才能作为最后一行的状态,也就是
dp[ n ][ 2m-1 ],所以只要输出这个就成功了。
下面来看一些具体过程:
(1)初始化:dp自然是要有初始化的,那么我们需要初始化的就是第一行的情况。下面是调用函数将dp[ 1 ][ num ](0<=num<=2m-1)初始化的过程。:此函数中的j是代表这一行中的第j+1列,通过位运算可以快速判断该位置的数值对应。

int init(long long num)
{
    for(int j=0;j<m;)
    {
        if(num & 1<<j)//因为是第一行,所以只要为1就必定要横放
        {
            if(j==m-1)//一次放两个格子,又要横放,以最后一个为起点肯定不行
                return 0;
            if(num & 1<<(j+1)){//横放成功
                j+=2;
                continue;}
            return 0;
        }
        j++;//竖放
    }
    return 1;
}

long long bit=(1<<m)-1;
memset(dp,0,sizeof(dp));
for(int i=0;i<=bit;++i)
{
    if(init(i))
        dp[1][i]=1;
}

(2)检查是否可以匹配:初始化完了,那么自然是检查相邻行之间每种情况的匹配了。注意检查是否能够匹配的同时其实也要判断i行的j情况本身是否可行。下面是检查函数,now代表的是现在所判断的行的情况对应整数,pre也就是上一行对应的一种情况的整数,查看他们能否匹配:

int check(long long now,long long pre)
{
    for(int j=0;j<m;)
    {
        if(now & 1<<j)//
        {
            if(pre & 1<<j)//上一行的第j个位置是1,那么必然此时要横放
            {
                if(j==m-1||!(now & 1<<(j+1))||!(pre & 1<<(j+1)))
                    return 0;
                j+=2;
                continue;
            }
            j++;是0的话,那么受到上一行,必定是竖放的下面一个
            continue;
        }
        if(!(pre & 1<<j))上一行该位置是0,此行相同列位置只能为1,否则错误。
        return 0;
        j++;
    }
    return 1;
}

(3)整合。刚才只是单个的判断单独一种情况的函数,那么我们还要通过合适的调用与整合,让他们正确的运行起来:

void work()
{
    long long bit=(1<<m)-1;
    memset(dp,0,sizeof(dp));
    for(int i=0;i<=bit;++i)
    {
        if(init(i))
            dp[1][i]=1;
    }
    for(int i=2;i<=n;++i)
        for(int j=0;j<=bit;++j)
             for(int k=0;k<=bit;++k)
    {
        if(check(j,k))
            dp[i][j]+=dp[i-1][k];
    }
    printf("%lld\n",dp[n][bit]);
}

那么最终的完整的代码就应该不是问题了:
:如果m>n我们可以让n,m交换一下数值,这样每行的状态数就少了很多,毕竟指数爆炸可是很厉害的,交换一下不过是让这个矩形翻转个90度。

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
long long dp[12][1<<12];
int n,m;;
int init(long long num)
{
    for(int j=0;j<m;)
    {
        if(num & 1<<j)
        {
            if(j==m-1)
                return 0;
            if(num & 1<<(j+1)){
                j+=2;
                continue;}
            return 0;
        }
        j++;
    }
    return 1;
}
int check(long long now,long long pre)
{
    for(int j=0;j<m;)
    {
        if(now & 1<<j)
        {
            if(pre & 1<<j)
            {
                if(j==m-1||!(now & 1<<(j+1))||!(pre & 1<<(j+1)))
                    return 0;
                j+=2;
                continue;
            }
            j++;
            continue;
        }
        if(!(pre & 1<<j))
        return 0;
        j++;
    }
    return 1;
}
void work()
{
    long long bit=(1<<m)-1;
    memset(dp,0,sizeof(dp));
    for(int i=0;i<=bit;++i)
    {
        if(init(i))
            dp[1][i]=1;
    }
    for(int i=2;i<=n;++i)
        for(int j=0;j<=bit;++j)
        for(int k=0;k<=bit;++k)
    {
        if(check(j,k))
            dp[i][j]+=dp[i-1][k];
    }
    printf("%lld\n",dp[n][bit]);
}
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        if(n==0&&m==0)
            break;
        if(n&1 && m&1){
            printf("0\n");
            continue;}
        if(n<m)
            swap(n,m);
        work();

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值