[状压dp] 蒙德里安的梦想(模板题+状压dp)

0. 前言

状压 dp 就是采用二进制数保存状态,方便进行位运算操作。例如 八皇后、八数码问题也都是采用了状态压缩的思想来使用一个二进制数唯一对应集合中的一个状态。

关键是要体会采用二进制数来表示状态的思想,要转变传统思维,学习接收并吸收这种思想。

1. 状压dp 模板题

291. 蒙德里安的梦想

在这里插入图片描述
重点: 状压 dp

首先,如果输入数据是一个 2 * m 的矩形方格的话,那么这个方案数就等价于 f[n] = f[n-1]+f[n-2] 就是一个非常经典的 fib 数列递推公式。

这道题将问题扩展为任意边长的矩形,和 fib 数列已经没了半毛钱关系。

经过分析可以发现,仅需找到所有横放的 1x2 小方格的方案数,那么竖放的方案就是唯一的,故方案数就能够求出来了,相当于是对问题的一步简化。

思路:

  • 状态定义:
    • f[i][j]:枚举第 i 列的第 j 个状态,j 状态为第 i-1 列有 1x2 的小方格伸到了第 i 列中的所在行。每一行对应一个二进制位,如果第 i-1 列有小方格捅到了第 i 列,那么该行对应的二进制位就置成 1,这样就能通过二进制位枚举出所有的情况了。
  • 状态转移:
    • 分类依据:主要需要注意三列的情况,即当前枚举的第 i 列,可能捅进第 i 列的第 i-1列,可能在第 i-1 列与第 i 列小方格冲突的第 i-2 列。总结来讲就是 i-2i-1i 这三列很重要
      • k 为第 i-1 列的 j,当 k = 10010,代表第 i-2 列中第一行、第四行有 1x2 的小方格捅到了第 i-1列。那么状态转移的第一个条件为:第 i 行的 j 与第 i-1 行的 k 在相同二进制位上不能同时为 1。一旦同时为 1,则该行的 1x2 小方格等于重叠了,即为失败状态。判断条件为 (j&k)==0
      • 同理,状态转移的第二个条件,每一列的 j 代表从前一列捅进来的小方格,再算上当前列捅向下一列的小方格,就是本列的所有小方格的位置,即 j|k,其不能存在连续奇数个 0,这样才能保证竖放小格子能成功。判断条件为 j|k 不存在连续奇数个 0
  • 状态初始化:f[0][0]=1,第 0 列只能是状态 0,没有前列则没有捅进来的格子。
  • 返回答案f[m][0] 因为第 m + 1 不能有格子捅进去。
  • 时间复杂度: 首先可以预处理得到状态转移的第二个条件。总共 11 个行列,那么二进制下就是 11 × 2 11 11\times 2^{11} 11×211 个状态,转移是 2 11 2^{11} 211 次,总的大概是 2^11 大概 2000 左右,即差不多 4*10^7 的计算量。

2020.10.11 字节笔试的时候考过本题,原题。

再强调一遍,本题非常非常非常经典,是状压 dp 入门经典题目。

代码:

#include <iostream>
#include <cstring>
#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) {
        memset(f, 0, sizeof f);
        
        for (int i = 0; i < 1 << n; ++i) {      
            st[i] = true;
            int cnt = 0;                        
            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;
        }
        
        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;
}

本题还有许多边界、初始化情况需要考虑。

  • 开数组为 12,因为题目要求限制为 11,且状态转移方程中有 i-1 出现,并且是需要求解到第 11 列下一列的状态的,所以选择 12 作为数组大小
  • long long 存方案数,dp 问题求方案数,不取模的情况下随随便便爆 int
  • 提前预处理 st 数组是错误的,因为当 n 不同时,其合法的状态也不同(细品)
  • 预处理过程中,cnt 存取连续 0 个长度,其实可以不必清 0。因为如果长度不合法,则必然存在一段长度是奇数的连续 0,那么此时的 cnt 就是一堆偶数再加上一个奇数,结果仍为奇数,还是能够判断出来的。且不存在奇数+奇数=偶数的误判,因为第一次奇数出现时,就能够将 st 对应位置直接置为 false
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值