题目描述
求把 N × M N\times M N×M的棋盘分割成若干个 1 × 2 1\times2 1×2的的长方形,有多少种方案。
例如当 N = 2 , M = 4 N=2,M=4 N=2,M=4时,共有 5 5 5种方案。当 N = 2 , M = 3 N=2,M=3 N=2,M=3时,共有 3 3 3种方案。
如下图所示:

输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数 N N N和 M M M。
当输入用例 N = 0 , M = 0 N=0,M=0 N=0,M=0时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1
≤
N
,
M
≤
11
1≤N,M≤11
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
算法思想(状压DP)
题目求把 N × M N×M N×M 的棋盘分割成 1 × 2 1×2 1×2的长方形的方案数。分析可知,只要确定了 1 × 2 1×2 1×2 小方块的横向摆放方式,纵向小方块的摆放方式也就确定了,有且只有惟一种对应方式。这样,题目可以转换成求 1 × 2 1 × 2 1×2 小方块的横向摆放的方案数。
状态表示
f[i][j]表示已经完成前 i 列,在其上一列(即i - 1列)伸出来的横向小方块状态为j的情况下,横向摆放小方块的方案数。
j是上一列小方块的状态,其二进制的每一位表示是否有伸出的横向小方块,例如有
5
5
5行方格,当
j
=
(
01001
)
2
j = (01001)_2
j=(01001)2 时,表示上一列(第2列)中,第
0
0
0行、第
3
3
3行伸出了小方块,如下图所示:

状态计算
计算f[i][j],判断第 i - 1 列状态能够转移过来的条件:
-
第
i - 1列伸出的状态k与当前列的状态j不能冲突(相同行不能同时为1),即j & k == 0,否则无法摆放。如下图所示: k = ( 00001 ) 2 k = (00001)_2 k=(00001)2与 j = ( 01001 ) 2 j = (01001)_2 j=(01001)2存在冲突。

-
在当前列摆放了横向小木块后,所有剩余的空白方格能够被竖向的小木块填满。那么在每一列所有连续的空白方格的个数必须是偶数,否则无法摆放竖向的小木块,即
k | j不能存在连续奇数个0。如上图所示,k|j= ( 01001 ) 2 =(01001)_2 =(01001)2,有奇数个0,表示 3 ∼ 4 3\sim4 3∼4行之间只有 1 1 1行,无法摆放竖向的小木块。
如果
j
j
j和
k
k
k满足上述两个条件,那么状态转移方程:
f
[
i
]
[
j
]
=
∑
f
[
i
−
1
]
[
k
]
f[i][j] = \sum f[i - 1][k]
f[i][j]=∑f[i−1][k]
初始状态
f[0][0] = 1表示第0列在其上一列没有伸出任何小方块格的情况下,横向摆放小方块的方案数为1(即不放置横向小方块)。
时间复杂度
- 状态数量: n × 2 n = 11 × 2 11 n\times2^n=11 \times 2^{11} n×2n=11×211
- 状态转移需要枚举的数量: 2 n = 2 11 2^n = 2 ^{11} 2n=211
时间复杂度: O ( 4 × 1 0 7 ) O(4\times10^7) O(4×107)
代码实现
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << N;
/*
f[i][j]表示已经完成前 i 列,在其上一列(即i - 1列)
伸出来的横向小方块状态为j的情况下,横向摆放小方块的方案数
*/
LL f[N][M];
//st[i]表示数i的二进制表示中是否不存在连续奇数个0
bool st[M];
int main()
{
int n, m;
while(cin >> n >> m, n || m)
{
//预处理st,计算所有列的状态为i时,是否不存在连续奇数个0
for(int i = 0; i < 1 << n; i ++)
{
int cnt = 0; //统计0的个数
st[i] = true;
for(int j = 0; j < n; j ++)
{
if((i >> j & 1) == 0) cnt ++;
else { //当前位是1
//如果cnt为奇数
if(cnt & 1)
{
st[i] = false;
break;
}
cnt = 0; //重新统计0的个数
}
}
//如果最后剩余0的个数为奇数
if(cnt & 1) st[i] = false;
}
//初始状态
memset(f, 0, sizeof f);
//初始状态,表示第0列在其上一列没有伸出任何小方块格的情况下,
//横向摆放小方块的方案数为1(即不放置横向小方块)
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 ++)
{
//满足状态转移条件,不冲突,并且不存在连续奇数个1
if((k & j) == 0 && st[k | j])
f[i][j] += f[i - 1][k];
}
}
}
//结果方案数为完成前m列,在每一列不伸出任何小方块的状态下的值
cout << f[m][0] << endl;
}
return 0;
}

本文介绍了一种使用状态压缩动态规划(状压DP)解决棋盘分割问题的方法。通过将N×M的棋盘分割成1×2的矩形,探讨如何计算不同的分割方案数量。文章详细解释了状态表示、状态计算及初始状态,并给出了具体的时间复杂度分析。

被折叠的 条评论
为什么被折叠?



