三个小时的恶战,怎么这两天的题这么难啊
题目:
求把 N×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
Java:
public class 状态压缩dp_蒙德里安的梦想 {
//N这里稍微开大了点是因为dp我们从1开始,自然就要开大于N
//st[i]表示某一行的状态是否合法
public static int N = 15, M = 1 << N;
public static boolean[] st = new boolean[M];
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (true) {
//接收每次的数据
int n = in.nextInt(), m = in.nextInt();
if (n == 0 && m == 0) break;
//预处理,判断合并列的状态i是否合法
//进制1表示横放,0表示竖放,如果不存在连续奇数个0,那么合法
//1<<n表示2的n次方,乘法原理,对于这一列的每一行,都应该有0或1两种选法,所有可能性就是n个2(组合数)
for (int i = 0; i < (1 << n); i++) {
int count = 0; //当前连续0的个数
boolean is_valid = true;
for (int j = 0; j<n; j++) {
if (((i >> j) & 1) == 1) { //如果为1
if ((count & 1) == 1) { //并且连续0的个数是奇数,不合法
is_valid = false;
break;
}
}
else count++; //不为1,说明是0
}
//这里是防止前置0的情况,比如4=0100,最后一次count++后for循环结束,但是最前端的0没有被算到
if ((count & 1) == 1) is_valid = false;
st[i] = is_valid; //更新状态
}
//状态计算
//记得每次都重置一下f,切记预处理,第0列不横放是一种合法方案,这个还是很难想到的
long[][] f = new long[N][M];
f[0][0] = 1;
//三个循环分别是:枚举列i,枚举第i列的状态j,枚举第i-1列的状态k
for (int i = 1; i<=m; i++) {
for (int j = 0; j < (1 << n); j++) {
for (int k = 0; k < (1 << n); k++) {
//判断两个条件(j和k不能有重叠的1)(j和k合并后必须合法,也就是不能有奇数个0,通过预处理的st判断)
//符合的话就状态转移即可,因为是方案数量,也就是dp中的count,所以是加起来
if ((j & k) == 0 && st[j | k]) {
f[i][j] += f[i-1][k];
}
}
}
}
System.out.println(f[m][0]); //输出本次的答案,也就是前m列已经摆好,且伸到m+1的格子数为0
}
}
}
C++
#include <cstring>
#include <cmath>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 15, M = 1 << N;
int n, m;
long long f[N][M];
bool st[M];
int main() {
while (cin >> n >> m, n || m) {
//二进制枚举处理每种状态(1表示横放,0表示竖放或者是从上一列横放捅过来的)
//如果不存在连续奇数个0,那么合法,
//1<<n表示2的n次方,乘法原理,对于这一列的每一行,都应该有0或1两种选法,所有可能性就是n个2(组合数),
for (int i = 0; i < (1 << n); i++) {
int c = 0;
bool is_valid = true;
for (int j = 0; j<n; j++) {
if (i >> j & 1) {
if (c & 1) {
is_valid = false;
break;
}
}
else c++;
}
if (c & 1) is_valid = false;
st[i] = is_valid;
}
//状态计算
//三个循环分别是:枚举列i,枚举第i列的状态j,枚举第i-1列的状态k
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++) {
//判断两个条件(j和k不能有重叠的1)(j和k合并后必须合法,也就是不能有奇数个0,通过预处理的st判断)
if (!(j&k) && st[j|k]) {
f[i][j] += f[i-1][k];
}
}
}
}
cout << f[m][0] << endl;
}
return 0;
}
思路:
经典y式dp法
1.状态表示
f[i][j]
:
表示i-1
列前(包括第i-1列)的所有列已经摆好,且从i-1
列伸到第i
列的方块数量(也就是第i-1
列横放的数量)为j
的所有方案,属性为Count。
2.状态计算
核心就是:
计算出所有横放长方形的方法,就是答案(因为横着放完了,剩下的地方用竖着的填充即可)
用二进制1和0表示对于当前格子,长方形是横着放的还是竖着放的(1表示横放,0表示竖放)
比如题目N=2,M=3:
三种方法:
1 1 0 | 0 1 1 | 0 0 0
1 1 0 | 0 1 1 | 0 0 0
一共有多少种可能性呢?
乘法原理,对于一列的每一行,都应该有0或1两种选法,所有可能性就是n个2,2^n(组合数)
也可以写作1 << n,
然后要计算f[i][j],前提是要保证合法性,也就是要满足两个条件:
1.第 i 列和第 i - 1 列不能有重叠的1,
(如果有重叠的1,说明有两个方格横放互相覆盖了,不合法)
2.第 i 列和第 i - 1 列合并后必须合法,也就是不能有奇数个0,通过预处理的st判断,
(如果有奇数个0说明有空着的地方,没填满,不合法)
符合上述条件后,终于可以状态转移了,也就是将第 i - 1 列的状态k的所有方案数转移过来,
(因为我们会枚举第 i 列的所有状态 j,对于状态 j,再枚举 i - 1列的所有状态 k,所以必定是覆盖到所有的case)
f[i][j] = f[i][j] + f[i-1][k]
(我们算的是总方案数,所以是加法)
这题真的不是光推dp就能理解的,推荐两个视频讲解吧(建议先看1再看2,思路就无比清晰了):
1.栗子和代码比较明确的b站up讲解:蒙德里安的梦想-状态压缩DP
2.偏抽象的y总讲解:AcWing 291.蒙德里安的梦想
两者的讲解都有可取之处,帮助很大,建议都看
声明:
算法思路来源为y总,详细请见https://www.acwing.com/
本文仅用作学习记录和交流