[AcWing] 291. 蒙德里安的梦想(C++实现)状态压缩dp例题

1. 题目

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

2. 读题(需要重点注意的东西)

思路:

状态压缩:即用二进制表示状态

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


代码实现思路:
基于以上思路,提炼出 :总方案数 = 横着放小方块的合法的方案数
所以只需要算所有横着放的方案数就可以
放横着的方块数且合法有如下两个条件:

① 放完横着的方块后,剩余每列的空余的方块中,任一连续的空方块数为偶数;(因为要放下 2 x 1 的竖着的方块)
② j & k == 0,即 j 与 k 不能在同一行,如果 j 与 k 在同一行,则产生了交集,会产生重叠

那么,代码流程:

bool st[M]; // 判断当前这一列空着的连续块是不是2的偶数倍(即判断这一列是否合法)
vector<int> state[M]; // 存储所有的合法状态
LL f[N][M];  // f[i][j] 表示从第i-1列伸到第i列的状态为j(j为一个二进制数,表示了各行中从第i-1列伸到第i列的状态,如若 j = 10100,表示第一、三行的第i-1列伸到了第i列)

① 构建 st 数组:找出所有空的连续块是2的倍数的情况;
② 通过 st 数组构建 state 数组:找出放横着的方块数且合法的所有的情况;
③ 通过 state 数组,进行dp,在每一个 f[ i ][ j ] ,执行for (auto k : state[j]),然后将所有合法的状态f[ i-1 ][ k ]加起来。

		// ----------------------③----------------------------
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                // 对于每个状态f[i][j] ,枚举所有在这列的合法的状态state[k],dp
                for (auto k : state[j])
                    // 将所有合法的方案加起来
                    f[i][j] += f[i - 1][k];

3. 解法

---------------------------------------------------解法---------------------------------------------------

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

using namespace std;

typedef long long LL;

const int N = 12, M = 1 << N; // M 最多是 2 的 N 次方

int n, m;
LL f[N][M]; 
vector<int> state[M]; // 存储所有的合法状态
bool st[M]; // 判断当前这一列空着的连续块是不是2的偶数倍(即判断这一列是否合法)

int main()
{   
    // 输入n和m,当n和m都为0的时候退出循环
    while (cin >> n >> m, n || m)
    {   
        // 循环2的n次方次,处理st数组?---------------问题一:为什么是2的n次方次循环
        // 这个循环其实表示的是每列的每种状态,如i = 1011下,当前这一列空着的连续块是不是2的偶数倍
        // 因为对于每一行,都有选和不选两种情况,则对n行,有2的n次方种选法,则要循环2的n次方次
        for (int i = 0; i < 1 << n; i ++ )
        {
            // i表示的是什么? i =(1011)2进制=(11)10进制
            int cnt = 0; // cnt表示0的个数
            bool is_valid = true;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1) // 如果当前位为 1,判断前面的空格的数量是不是偶数个-----------问题二:这行代码是什么意思?
                {
                    if (cnt & 1) // 判断前面的空格的数量是不是偶数个------------------问题三:这行代码是什么意思?
                    {
                        is_valid = false; // 不是偶数个,直接置位false
                        break;// break
                    }
                    cnt = 0; // 如果是偶数个,则从这行起,重新开始计算空格的数量
                }
                else cnt ++ ; // 如果当前位为0,则继续计数,cnt++
            if (cnt & 1) is_valid = false; // 遍历到末尾,如果最后这段0的个数是奇数个,则将is_valid置为false
            
            st[i] = is_valid; // 经过以上代码,便统计完这列了,将这列是否合法记在st数组中
        }
        
        // 枚举所有的合法状态
        for (int i = 0; i < 1 << n; i ++ )
        {
            state[i].clear();
            for (int j = 0; j < 1 << n; j ++ )
                // 满足放置横着的1x2块的合法的两个条件
                if ((i & j) == 0 && st[i | j]) //C++中, | 表示按位或
                    // 将j存到state[i]----------------问题四:这行代码是什么意思?
                    state[i].push_back(j); // 将i列的合法的行j存在state[i]中
        }
        
        
        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                // 对于每个状态f[i][j] ,枚举所有在这列的合法的状态state[k],dp
                for (auto k : state[j])
                    // 将所有合法的方案加起来
                    f[i][j] += f[i - 1][k];

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

    return 0;
}

可能存在的问题(所有问题的位置都在上述代码中标注了出来)

问题一: 循环2的n次方次,处理st数组时,为什么是2的n次方次循环?
问题一答: 因为对于每一个状态,都有伸或者不伸两种情况,则对于n行,最多有2的n次方个方案。

问题二: if (i >> j & 1)这行代码是什么意思?
问题二答: i 右移 j 位与上 1 ,这行代码是判断第 j 位上是不是1,如果是1了,表明这位上不是0,则进一步去判断前一段的连续的0是不是偶数个

问题三: if (cnt & 1) 这行代码是什么意思?
问题三答: 按位与可以判断整数的奇偶性。这行代码的作用是判断前面的空格的数量是不是偶数个,在C++中, &是按位与,如果cnt = 3 , 则 cnt & 1 = 1,如果cnt = 4 , 则 cnt & 1 = 0。
即,奇数 & 1 = 1,偶数 & 1 = 0。
在这里插入图片描述
在这里插入图片描述

原理:
与操作:都为1才为1
一个数的奇偶性由这个数的二进制最后一位决定,最后一位是1,就是奇数,是0就是偶数;
利用按位与的清零功能判断

问题四: state[i].push_back(j); 将j存到state[i],这行代码是什么意思?
问题四答: 将第 i 列的所有的合法的行 j 存在 state[i] 中

4. 可能有帮助的前置习题

5. 所用到的数据结构与算法思想

  • 动态规划
  • 状态压缩dp

6. 总结

状态压缩dp的例题,理解思想并自行推导出代码。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Cloudeeeee

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值