AcWing 291. 蒙德里安的梦想
题目描述
求把 N×MN×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。
例如当 N=2,M=4时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。
如下图所示:
输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数 N 和 M。
当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1≤N,M≤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
算法求解
分析
先把所有能横着的小长方块放上,剩下的竖着的小长方块就只有一种放法
所以总的方案数 = 放横着小长方块的数目
用f[i][j]
表示:横着放小长方块第i
列处于状态j
的时候的总的方案数
- \(j==0\)表示:第
i-1
列没有小长方块捅到第i列 - \(j== (10010)_2\),表示第
i-1
列的第1、4行有横着的小长方块捅到第i
行
要使得第i
列能放横着的小长方块,并且放完小长方块后,第i-1
列还能竖着放小长方块,使用j
表示放完小长方块后第i
列的状态,k
表示放完后第i-1
列的状态。第那么要求两点,
- 第
i
列和第i-1
列放的横小长方块不能冲突:j&k == 0
- 放完第i列的小长方块后,第i-1列连续0的数量不能是奇数:
st[j&k] == true
其中每个状态是否合法,可以预处理。
然后是dp过程
注意f[0][0] = 1
表示第0列没有格子捅出来的情况只有一种(什么都不放)
返回的时候返回f[m][0]
(其实一共只有 0~m-1
列),表示f[m][0]
第m列没有格子捅出来的总情况有多少种。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 12; // 一共多少列
const int M = 1 << N; //每列的状态数
LL f[N][M]; // f[i][j]表示当第i列处于状态j的时候的状态数
bool st[M]; // 表示某个状态是否合法,当有连续奇数个0的时候不合法
int n, m;
int main()
{
while(1)
{
scanf("%d%d", &n, &m);
if(n == 0 && m == 0) break;
// 标记所有合法/不合法状态
for(int i = 0; i < 1 << n; i++)
{
// i 表示一个状态
int cnt = 0;
st[i] = true; // 假定其合法
// 看i的每一位
for(int j = 0; j < n; j++)
{
// 如果i的第j位为1的话
if(i>>j & 1)
{
// 前面有奇数个0
if(cnt & 1)
{
st[i] = false;
break;
}
cnt = 0; // 遇到1了,前面0的数量清0
}
else cnt++;
}
if(cnt & 1) st[i] = false; // 判断一下最后一段是不是奇数个0
}
memset(f, 0, sizeof f);
f[0][0] = 1; // 第0列没有横着的长方块捅出来的方案数为1
for(int i = 1; i <= m; i++) // 枚举所有的列
{
for(int j = 0; j < 1 << n; j++) // 枚举第i列所有状态
{
for(int k = 0; k < 1 << n; k++) // 枚举第i-1列所有的状态
{
// j&k=0表示的是第i列和第i-1列没有冲突的行
// j|k的二进制表示将第i列放了横着的长方块后,第i-1列有多少个1
if(((j&k) == 0) && st[j|k]) // 注意加括号,有优先级
{
f[i][j] += f[i-1][k];
}
}
}
}
cout << f[m][0] << endl;
}
return 0;
}
时间复杂度
参考文章
- 所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩。
- 本题等价于找到所有横放 1 X 2 小方格的方案数,因为所有横放确定了,那么竖放方案是唯一的。
- 用f[i][j]记录第i列第j个状态。j状态位等于1表示上一列有横放格子,本列有格子捅出来。转移方程很简单,本列的每一个状态都由上列所有“合法”状态转移过来
f[i][j] += f[i - 1][k]
- 两个转移条件:
i
列和i - 1
列同一行不同时捅出来; 本列捅出来的状态j和上列捅出来的状态k求或,得到上列是否为奇数空行状态,奇数空行不转移。- 初始化条件
f[0][0] = 1
,第0列只能是状态0,无任何格子捅出来。返回f[m][0]
。第m + 1列不能有东西捅出来。#include<bits/stdc++.h> using namespace std; const int N = 12, M = 1 << N; int st[M]; long long f[N][M]; int main(){ int n, m; while (cin >> n >> m && (n || m)){ for (int i = 0; i < 1 << n; i ++){ int cnt = 0; st[i] = true; for (int j = 0; j < n; j ++) if (i >> j & 1){ if (cnt & 1) st[i] = false; // cnt 为当前已经存在多少个连续的0 cnt = 0; } else cnt ++; if (cnt & 1) st[i] = false; // 扫完后要判断一下最后一段有多少个连续的0 } memset(f, 0, sizeof f); 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])) // j & k == 0 表示 i 列和 i - 1列同一行不同时捅出来 // st[j | k] == 1 表示 在 i 列状态 j, i - 1 列状态 k 的情况下是合法的. f[i][j] += f[i - 1][k]; cout << f[m][0] << endl; } return 0; } 作者:sjytker 链接:https://www.acwing.com/solution/content/5121/ 来源:AcWing 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
作者:sjytker
链接:https://www.acwing.com/solution/content/5121/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。