[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的例题,理解思想并自行推导出代码。