AcWing 291. 蒙德里安的梦想(棋盘式状压dp)

在这里插入图片描述
在这里插入图片描述

题意:

求把N * M(1≤N,M ≤11)的棋盘分割成若干个1*2的长方形,有多少种方案。

例如当N=2,M = 4 时,共有5种方案。如下图所示:

在这里插入图片描述

当N=2,M =3时,有3种方案。如下图所示:

在这里插入图片描述

思路:

数据范围非常小,N<=11,这提示我们可能用状态压缩dp求解。

本题核心

先放横着的,后放竖着的,可以发现,当把所有横着的处理完后,竖着的就只有一种填充方案,比如下图,红色部分包含两横放的方块,剩下部分直接用竖着的方块填充即可:
在这里插入图片描述
因此,在计算方案数的时候,我们可以统计:如果我们只放横着的方块的话,合法的摆放方案有多少种
总方案数 = 只放横着的小方块的合法方案数

接下来的问题就是:如何判断当前方案是否合法?所有剩余位置能否填充满竖着的小方块。

对此问题我们可以按列分析每一列内部 所有 连续未填充的 小方块是偶数个。(这样才可以填满)

接下来分析一下如何用动态规划处理这道题(闫氏dp分析法)

状态表示

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

解释

  • “已经将前 i - 1 列摆好”:由于前 i-1 列已经摆好,因此现在不会新增方块再摆到前 i-1 列了。
  • “从第 i-1 列伸到第 i 列的状态为 j”:如下图,i=2,有第 0、1、4 行的方块从第 i-1 列伸到第 i 列,其二进制表示状态为 j = (11001)
  • 此时 f[2, (11001)] 则表示为: i 列摆完 之后,从第 i-1 列伸到第 i ,且 状态形如 j 方案 的集合(不一定只有一种,因为在此之前的摆放方式是不确定的,这里是用某一个状态表示某一类方案,这是一个“化零为整”的过程)
  • 在这里插入图片描述

状态计算

(考虑 f[i, j] 如何计算出来,对应的是一个化整为零的过程)

f[i, j] 表示成一个 椭圆状集合,想办法将椭圆进行分割,使得每一部分都可以被计算出来,最终的结果应当是每一部分的方案数量之和

划分依据最后一步不同的操作

解释:由于 最后一步从第 i-1 列到第 i 列的方案 已经固定,我们进行 集合划分 应该找的是 最后一个不同点,即 从第 i-2 列伸到第 i-1 的方案,

因此我们可以依据 i-2 列伸到第 i-1 列的状态 将椭圆分割成若干类

f[i ,j] 表示的椭圆 最坏情况下分割为 2^n 类子集,因为 n 位中每一位有 0、1 两种情况。每一类子集都表示一个 从第 i-2 列伸到第 i-1 的具体状态,比如我们将其设为一个二进制数 k

如果第 i-2 列到第 i-1 列只有第 2 行伸出,那么 k = (00100)。如下图绿色部分
在这里插入图片描述

上述的划分方案是不重不漏的,划分的所有部分 相互之间无交集且一起构成了所有的方案

在这里插入图片描述

现在我们想想 划分的每一类如何求解

假设 从第 i-2 列伸到第 i-1 的状态为 k从第 i-1 列伸到第 i 的状态为 j,显然 方案数量 根据定义为 f[i-1, k],我们更要注意的是 k、j 两个状态能 构成一个合法方案 的条件:

  • 首先上图中绿色方块和红色方块不能处于同一行,否则会产生交集,不合法。即 不能在同一行都有 1两者相与结果为 0,代码表示为 i & k == 0
  • 对于第 i-1 列,空余小方格的位置已经固定(联系 f[i, j] 的含义即可),这些空余的位置可以 完全被竖着的 1×2 方块填满,因此条件为:所有连续空余的位置长度必须为偶数

只要满足上面两个条件,就说明 k、j 这两个状态是没有冲突的,我们将所有无矛盾的状态 k 累加起来,就得到了 f[i, j] 的值

设下标从 0 开始,共有 m 列,最后一列为 m-1 列,本题 最终答案 为:f[m, 0](前 m-1 列已经摆好,且从第 m-1 列伸到第 m 列的状态为 0,其恰为摆满 n × m 棋盘的所有方案)
AcWing 291. 蒙德里安的梦想

时间复杂度:

两维状态,第一维最大为 11,第二维最大为 2 ^ 11,状态计算最多有 2 ^ 11中情况,因此最坏情况下时间复杂度为 11 × 2 ^ 11× 2 ^ 11 ≈ 4e7,而且还有多组测试数据,因此本题时间限制开到了 3s

代码:

为了保证不超出时间限制,我们将代码做些优化:预处理对于每个状态 j 而言,有哪些状态 k 可以更新成 j ,这样一来我们就无需每次都要将 j、k 循环判断。

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;
const int N = 12; //由于最后后处理到 m+1(从0开始计数) 列,因此这里要加一

int n, m;
ll dp[N][1<<N];
vector<int> state[1<<N]; //存储合法状态,state[i] 表示一维数组中的元素可以合法转移到 i
bool st[N]; //判断某个状态是否合法:当前列是否能由 1×2 方格填充满,当前列所有连续空余的长度是否为偶数

void init1() //预处理st数组
{
        for(int i=0; i<1<<n; ++i)
        {
                int cnt = 0; //连续空余部分0的个数
                bool is_valid = true; //表示是否合法
                for(int j=0; j<n; ++j) //枚举每一位
                        if(i>>j&1) //当前位为1
                        {
                                if(cnt&1) //判断当前这位 1 是否有奇数个
                                {
                                        is_valid = false; //如为奇数说明不合法
                                        break;
                                }
                                cnt = 0; //由于我们记录的是连续的0,因此出现 1 则要将 cnt 清 0
                        }
                        else ++cnt; //否则继续计数
                if(cnt&1) is_valid = false; //最后一段0是奇数也是不合法的
                st[i] = is_valid;
        }
}

void init2()
{
        for(int i=0; i<1<<n; ++i) //枚举所有合法状态
        {
                state[i].clear();
                for(int j=0; j<1<<n; ++j)
                {
                    //满足两条:①两状态的同一行不能有两个1,即两状态每行都是0对1,相与为0
                    //②所有填满的位置是合法的(i、j两状态取并集 如101 | 010 = 111)
                        if((i & j) == 0 && st[i | j]) 
                                state[i].push_back(j); //说明j可以合法转移到i
                }
        }
}

int main()
{
        while(cin>>n>>m, n||m)
        {
                init1(), init2(); //预处理函数
                
                memset(dp, 0, sizeof dp);
                dp[0][0] = 1; //初始化,空棋盘时有一种方案
                for(int i=1; i<=m; ++i)
                {
                        for(int j=0; j<1<<n; ++j)
                        {
                                for(auto k : state[j])
                                        dp[i][j] += dp[i-1][k];
                        }
                }
                cout<<dp[m][0]<<endl;
        }

        return 0;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值