AcWing 291. 蒙德里安的梦想
题目
求把 \(N \times M\) 的棋盘分割成若干个 \(1 \times 2\) 的长方形,有多少种方案。
例如当 \(N=2,M=4\) 时,共有 \(5\) 种方案。当 \(N=2,M=3\) 时,共有 \(3\) 种方案。
如下图所示:

输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数 \(N\) 和 \(M\)。
当输入用例 \(N=0,M=0\) 时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
\(1 \le N,M \le 11\)
输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205
题解

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << N; //1 << N 表示2^N-1 最多有N行,则每一列最多N个格子, 每个格子都有0或1两个状态,则一列的格子共有2^N个状态可能
LL f[N][M]; //状态方案数过大 我们用longlong存储, f[i][j] 第一维表示第i列, 第二维表示第i列所有可能的状态
bool st[M]; //st数组用来判断每一列的每个状态是否合法
//每种状态是否有奇数个连续的0,如果奇数个0是无效状态(false),如果是偶数个零置为true。如果有连续偶数个0,就可以将所有空格子全部竖着摆棋子
vector<int> state[M]; //二维数组记录最终合法的状态
int n, m;
int main()
{
while (cin >> n >> m, n || m) //只要不是n和m都是0(题目规定的非法输入)就继续读入 注意要把cin放在while循环判断条件里面,因为有多组测试数据
{ //第一部分预处理 当每一列连续0的个数是奇数时,就摆不下竖着的棋子会有空位 非法状态
//对于每种状态,先预处理每列不能有奇数个连续的0,将结果状态记录在st[]内
for (int i = 0; i < (1 << n); i ++ ) //一列的格子一共有2^n个状态 我们遍历这些状态
{ //例如i=100100..00表示当前状态是只有第一个和第四个格子摆了棋子
int cnt = 0; //记录连续0的个数
bool is_valid = true; //某种状态没有奇数个连续的0则标记为true,初始值为true
for (int j = 0; j < n; j ++ ) //内层循环从上到下遍历这一列
{
if ( (i >> j) & 1)
{
//i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位;
// &1为判断该位是否为1,如果为1进入if
if (cnt & 1) //该位为1时,看前面0的个数,也就是cnt,cnt的值如果是奇数(cnt &1为真) 该状态i就不合法 直接结束这次整个循环
{
is_valid = false;
break; //break掉内层循环
}
cnt = 0; //若进行到这里表示没有进入到上面的if判断里,即cnt是偶数个,当前状态就是合法的,我们将计数器cnt清零,方便判断下一次状态
}
else cnt ++ ; //当不满足if ( (i >> j) & 1)判断,即i状态的当前位j是0,计数器++
}
if (cnt & 1) is_valid = false; //当j循环到j= n-1时,也就是最下面一格时,如果是状态i的当前位是0,只会执行cnt++,我们需要在内层循环外部判断一下最下面一个元素此时连续0是否为奇数
st[i] = is_valid; //如果当前状态i所有位数都满足没有奇数个连续0的要求,is_valid就仍为true,反之为false
}
//第二部分预处理
//经过上面每种状态 连续0的判断,已经筛掉一些状态。
//下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突
for (int j = 0; j < (1 << n); j ++ ) //还是枚举第i列的所有状态
{
state[j].clear(); //清空上次循环留下的数据
for (int k = 0; k < (1 << n); k ++ ) //对于第i-1列的所有状态
{
if ((j & k) == 0 && st[j | k]) //注意||与|的区别
//(j & k) == 0 成立 表示第i-2列伸到第i-1列的 和从第i-1列开始摆的棋子不冲突(不在同一行),
//(j & k) == 0判断i-1与i-2横方块有无冲突 由于j和k都是用二进制数表示当前列的状态 1&0=0 1&1=1 判断两列中同一行的格子是否冲突 例如 10010 & 01101 ==1 不冲突
//解释一下st[j | k]
//已经知道st[]数组表示的是这一列有没有连续奇数个0的情况,
//我们要考虑的是第i-1列(第i-1列k是这里的主体)中从第i-2列横插过来的,
//还要考虑自己这一列(i-1列k)横插到第i列的
//比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
//那么合在第i-1列,到底有多少个1呢?
//自然想到的就是这两个操作共同的结果:两个状态或一下。 j | k = 01000 | 10101 = 11101
//这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的
//将或之后的结果放在st[]里判断当前i-1列的实际状态是否是合法的
state[j].push_back(k); //如果if里的判断条件成立
//存入状态j的合法的真正可行的相邻状态k
//“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
}
}
//开始dp(也在while循环内)
memset(f, 0, sizeof f);
//全部初始化为0,因为是连续读入,不能让上一组的测试用例f[]][]影响这一组,这里是一个清空操作。
f[0][0] = 1;// 这里需要回忆状态表示的定义
//按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
//首先,这里没有-1列,最少也是0列。
//其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。
for (int i = 1; i <= m; i ++ ) //遍历每一列:第i列合法范围是(0~m-1列) f[0][0]已经定义过
{
for (int j = 0; j < (1 << n); j ++ ) //遍历每一列里的每一种状态
{
for (auto k : state[j]) //遍历第i-1列的合法状态k
f[i][j] += f[i-1][k]; //当前列的方案数就等于之前的第i-1列所有合法状态k的累加。
}
}
//最后答案是什么呢?
//f[m][0]表示 前m-1列都处理完,并且第m-1列没有一个棋子伸到第m列的所有方案数。
//整个棋盘共m-1列
//即整个棋盘处理完的方案数
cout << f[m][0] << endl;
}
return 0;
}

浙公网安备 33010602011771号