【Acwing291】蒙德里安的梦想(状态压缩dp)详细讲解

题目描述

题目分析

显而易见的重要事实

首先,需要明白一个很重要的事实:

所有的摆放方案数=所有横着摆放且合理的方案数

这是因为,横着的确定之后,竖着的一定会被唯一确定,举一个例子:

------唯一确定----->

所以使用动态规划进行状态表示的时候,仅仅需要考虑横着的长方形即可

状态表示

随后,我们来看状态表示:

f[i,j]表示:前i-1列已经摆好,且从第i-1列伸出到第i列的状态为j的所有方案数

注意:列数的下标是0~m-1

举一个例子:

 如上图,假设一个6*6棋盘,前两列已经以如图方式摆好,此时对于第三列的第一、四行有被第二列伸过来的部分,所以此时第二列的状态就是100100,将这个字符串看成二进制数,转为十进制为36,所以这就是f[2,36]所指的“所有方案数”的其中一个。

状态计算

对于同一列i,即使j值(状态数)相同,也会因为前i-1列的摆放方案不同而导致整体的摆放方案不同,也就是说,在前i-1列已经确定的情况之下,第i-1列的每一个状态的每一个方案都能够组合一个第i列的确定的状态j得到一个新的摆放方案:

比如上述这个图,列3的状态为:101010,由于列2可能存在不同的状态(0x0y0z),导致整体的摆放方案不同,同理,列2的某一种状态的方案数也因为列1可能存在不同的状态导致此方案数有可能大于1,所以呈现一个递归的现象,由此可得状态计算:

f[i,j]=\sum f[i-1,k]  

其中k为第i-1列的状态值,可以取不同的值

何时合理

显然,对于上述状态计算式子,如果不限制k值和j的关系,而一味地让k和j取遍整个0~2^n-1,就会发生一些不合理的现象,比如:

假设现在已经处理到i=3的情况,并且假设i=0~i=2的所有长方形已经摆好,就会发现两个不合理的现象:

1.在第一行的列2和列3地方发生了交叉

2.第二行的列2位置有一个不合理空缺,这个位置无法再填补任何的长方形,因为现在已经假定0~2列是摆好的,不会再去这个位置填补空缺的横着放的长方形,同时这个地方也根本不可能去放置竖长方形。

为什么产生了这个现象?

因为没有约束k和j的关系

我们先来看第一个不合理之处产生的原因:(k&j)!=0

顺便注意一下这里,k&j一定要加括号,因为!=的优先级大于&

因为如果k&j!=0,那么至少存在一行使得i和i-1列中同时有从i-1和i-2列伸过来的长方形,从而导致长方形的重叠

第二个不合理之处的产生原因:i-1列中的连续的空缺位置中存在空缺大小为奇数的情况

如何避免这个不合理之处呢?

保证:st[j|k]==true,其中st[x]为true的条件是x的二进制表示中没有奇数个连续的0

边界问题

开头已经说过了,此题列的表示下标从0开始,所以初始化为f[0][0]=1,意思是“没有从左边的棋盘边界的外边伸向第一列的长方形的方案数为1”,其它f全部初始化为0。

如果列数为m,列号从0开始,在遍历i的时候,最后遍历到的下标是m而不是m-1,因为当遍历到第i列的时候,实际上保证的是第i-1列的合法性,比如,刚才那个例子:

当遍历到i=3的时候,发现的两个不合理之处全在i=2列。

最后的答案为:f[m][0],意思是“在0~m-1列全部摆好的情况之下,棋盘右边界的右边一列没有第m列伸过来的长方形的总方案数”

朴素代码以及短路现象

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12, M = 1 << N;

int n, m;
long long f[N][M];
bool st[M];

int main()
{
    while (cin >> n >> m, n || m)
    {
        for (int i = 0; i < 1 << n; i ++ )
        {
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1)
                {
                    if (cnt & 1) st[i] = false;
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) st[i] = false;
        }

        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                for (int k = 0; k < 1 << n; k ++ )
                    if ((j & k) == 0 && st[j | k])
                        f[i][j] += f[i - 1][k];

        cout << f[m][0] << endl;
    }
    return 0;
}

在上y总的课时,y总说这个代码有一个神奇的现象,就是把(j & k) == 0 && st[j | k]中&&前后的部分进行交换之后,时间会变长许多,此代码从892ms变为1442ms,很显然这与&&的短路性有关,当

(j & k) == 0不满足时,就会跳过st[j | k]的判断,因为(j & k) == 0的“成功率”小于“st[j | k]”的成功率,所以会出现上述现象。

优化之后的代码

此代码已经经过优化:预处理去除无效状态

#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
const int N=12,M=1<<N;
typedef long long LL;
LL f[N][M];
bool st[M];
vector<int>state[M];//存放j和k的合法映射的集合
int n,m;
int main()
{
    while(scanf("%d%d",&n,&m)==2&&(n||m))
    {
        for(int i=0;i<1<<n;i++)//st数组的预处理
        {
            int cnt=0;
            st[i]=true;
            for(int j=0;j<n;j++)
            {
                if(i>>j&1)
                {
                    if(cnt&1)
                    {
                        st[i]=false;
                        break;
                    }
                    cnt=0;
                }
                else 
                cnt++;
            }
            if(cnt&1)st[i]=false;
        }
        for(int j=0;j<1<<n;j++)
        {
            state[j].clear();//为什么要清空?因为是多样例输入
            for(int k=0;k<1<<n;k++)
            if((k&j)==0&&st[k|j])
            state[j].push_back(k);//满足推出f[i][j]的第i-1行的其中一个状态数k
        }
        memset(f,0,sizeof f);//因为是多样例输入
        f[0][0]=1;
        for(int i=1;i<=m;i++)
        {
            for(int j=0;j<1<<n;j++)
            for(auto k:state[j])//代码优化的体现,大大减少了冗余
            f[i][j]+=f[i-1][k];
        }
        cout<<f[m][0]<<endl;
    }
    return 0;
}

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值