You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##AcWing 291. 蒙德里安的梦想

一、题目描述

求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。

例如当 N=2M=4 时,共有 5 种方案。当 N=2M=3 时,共有 3 种方案。

如下图所示:

输入格式 输入包含多组测试用例。

每组测试用例占一行,包含两个整数 NM

当输入用例 N=0M=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

二、核心思想

1、先放横的再放竖的

理由:如果把横的全部放完,其它的空位用竖的放就行啦,此时,竖的放法只有一种。

2、总方案数等于只放横的 合法方案数

Q:如何判断方案是否合法? 当前摆完横着的小方块之后,剩余的位置,如果能用竖着的小方块 填满 就合法,否则不合法。 具体的办法按列看

  • 如果每一列所有连续空着的小方块个数是偶数个,可以用竖的填满
  • 如果每一列所有连续空着的小方块个数存在奇数个,必然填充不满

三、动态规划

1、状态表示

\large f[i][j]:已经摆完前i列并且第i列的状态为j的所有方案。

对于j这个二进制数中的1是指我在这个小方格 放置了一个横条。也就是这个小方格是一个横条的起始位置。

j是一个二进制数,用来表示哪一行的小方块是横着放的,其位数和棋盘的行数一致。 具体的实现:用一个N位(N是指行数)的二进制数,每一位0/1表示不同的状态,0 → 2^N 1 N 二 进 制 对 应 的 十 进 制 数 )中的所有数来枚举全部的状态。

解释:上图中 i=2j =10101二进制数,但是存的时候用十进制 所以这里的f[i][j] 表示的是所有前i-1列摆完之后,从第 i-1列伸到第i列的状态是10101(第1行伸出来,第3行伸出来,第5行伸出来,其他行没伸出来)的方案数。

2、初始化

初始化f[0][0]=1也好理解一些。因为我们正常摆第一列的时候是随便自由摆放的,所以f[0][i]中除了f[0][0]其他的都应该是非法的,赋值为0(因为如果i不是0,就代表有些小方格会放置横条,就会影响到第一列的摆放),而对于f[0][0]其含义是已经摆完了前0列并且第0列的状态是0,也就是这一列没有任何一个小方格放置了横条。其摆法只有一种所以赋值为1

3、答案在哪

对于f[m][0]也就是已经摆完第m列,并且第m列的状态为0,也就是第m列没有任何一个小方格新去摆放横条,这正好就是我们想要的结果。

4、状态转移

i-1列的某种状态,与第i列的某种状态之间,可能能转移,也可能无法转移。

思考什么情况下会不存在转换关系?

(1)、寻找兼容状态

如果i-2想在当前行 伸出 一个小方格,而i-1列也想向下一列 伸出 一个小方格,就是冲突。

代码解读:对应的代码为(i & j ) ==0 ,表示 ij没有1位相同,即没有1行有冲突。此处的位运算大大提高了两种状态冲突检测的效率!,如果不用位运算,循环应该是跑不了的!

结论:通过分析判断出冲突状态,也就是找出了所有状态间的兼容关系,每个状态,只能从与自己兼容的状态转移过来,这是可以提前预处理出来的。

(2)、无效状态检查

是不是状态不冲突就可以转化了呢?不是的,举个栗子吧:两种状态不冲突:

i-2列有一个状态:00100,i-1列有一个状态:01010,它们两个之间是没有重叠的,不违反上面的第一条规则,但依然是有问题的:

它会造成i-1 列无法继续用竖着的小方格填充满!!!

既然从第i-1列到第i列横着摆的,和第i-2列到第i-1列横着摆的都确定了,那么第i-1列 空着的格子就确定了,这些空着的格子将来用作竖着放。如果某一列有这些空着的位置,那么该列所有连续的空着的位置长度 必须是偶数

结论:啥样的状态是有效的,啥样的状态是无效的,是可以提前处理出来的。

三、实现代码

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 12;
const int M = 1 << N;
int n, m;

LL f[N][M];       // 已经摆完前i列并且第i列的状态为j的所有方案
vector<int> v[M]; // 对于每个状态而言,能转转移到它的状态有哪些,预处理一下(二维数组)
int ok[M];        // 某种状态是否合法就是是不是存在奇数个连续0

int main() {
    while (cin >> n >> m, n + m) {
        // ① 预处理枚举行数n的每个二进制位,可以枚举出每种可能出现的状态对应的二进制表示,这些状态有些是不合法的
        // 只有不存在连续奇数个数字0的状态才是合法的一旦n确定了这个有效状态是可以预处理出来的
        for (int i = 0; i < 1 << n; i++) {
            int cnt = 0;                // 连续0的个数
            ok[i] = 1;                  // 初始设置此状态是合法的
            for (int j = 0; j < n; j++) // 遍历此状态的每一个二进制位,开始检查
                if (i >> j & 1) {       // 如果本位是1,表示连续0发生中断需要统计连续0的个数并且记得清空cnt
                    if (cnt & 1) {      // 奇数个连续0, (cnt & 1) = (cnt % 2 >0)
                        ok[i] = 0;      // 连续0发生中断,此状态为不合法状态
                        break;          // 不用再往后看了,一次失足就不挽救
                    }
                    cnt = 0; // 连续个零计数重新开始
                } else
                    cnt++; // 连续0的计数器++

            // 最后一个cnt++后依然可能有连续奇数个举个栗子n=4=(0100)_2完成数位枚举后cnt=1,也就是高位存在奇数个0
            if (cnt & 1) ok[i] = 0;
        }
        // ② 预处理:枚举每个状态,获取可能是从哪些有效状态转移过来
        for (int i = 0; i < 1 << n; i++) {
            // 多组数据,每次预处理时清空一下
            // vector<int> v[M] 是一个二维数组初始化比较麻烦需要用循环遍历第一维然后再v[i].clear()进行清空
            v[i].clear();

            // 状态i,从哪些状态转化而来?
            for (int j = 0; j < 1 << n; j++) // j为前序状态
                // (1) i & j==0 同一行不能同时探出小方格,那样会有重叠
                // (2) 解释一下ok[i | j]
                // 比如: 01000 | 10101 = 11101,描述当前完成状态叠加后的最终状态,在预处理的数组中找一下,是不是合法状态
                if ((i & j) == 0 && ok[i | j]) v[i].push_back(j);
        }

        // 多组数据,每次清零
        memset(f, 0, sizeof f);
        // 初始方案数
        f[0][0] = 1; // 可以理解为 从虚拟的第0开始第一个0还没有向右探出小方格第二个0此时的方案数只有1种。
        // 如果你想为什么不是0种下面的递推关系就会让你明白0做为基底就啥也递推不出来了。

        // DP正式开始
        for (int i = 1; i <= m; i++)
            for (int j = 0; j < 1 << n; j++) // 遍历第i列的所有状态j
                for (auto k : v[j])          // 遍历第i-1列的所有状态k
                    f[i][j] += f[i - 1][k];  // 每个合法状态,均需从它的前序有效状态转移而来
        // 输出答案
        cout << f[m][0] << endl;
    }
    return 0;
}