题目传送门

一、核心思想

1、先放横着的,再放竖着的。

2、总方案数等于只放横着的小方块的合法方案数。

为什么呢?因为如果把横着的全部放完之后,其它的空用竖着的来放就行啦。如果横的放完了,竖着的方案数是唯一的。

那如何判断方案是否合法?

当前摆完横着的小方块之后,所有剩余的位置,如果能填充满竖着的小方块就是合法,否则不合法。进一步思考知道:可以按列来看,每一列只要所有连续的空着的小方块个数是偶数个,就可以用竖着的去填充满。如果存在奇数个连续空着的小方块,必然填充不满。

二、状态表示

这是一道动态规划的题目,并且是一道状态压缩的\(dp\):用一个\(N\)位的二进制数,每一位表示一个物品,\(0/1\)表示不同的状态。因此可以用\(0 → 2^N − 1\)\(N\) 二 进 制 对 应 的 十 进 制 数 )中的所有数来枚举全部的状态。

\(f[i][j]\) 表示:已经将前 \(i -1\) 列摆好,且从第\(i − 1\)列伸出到第\(i\)列的状态是\(j\)的所有情况。其中\(j\)是一个二进制数,用来表示哪一行的小方块是横着放的,其位数和棋盘的行数一致。 请看下面的释义:

AcWing 291. 蒙德里安的梦想

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

三、状态转移

\(i-1\)列的某种状态,与第\(i\)列的某种状态之间,是可能存在转换关系的,也可能是不存在转换关系的。

讨论一下什么情况下会不存在转换关系:

(1)、状态冲突

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

AcWing 291. 蒙德里安的梦想

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

(2)、后序状态无效

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

\(i-2\)列有一个状态:\(00100\),\(i-1\)列有一个状态:\(01010\),它们两个之间是没有重叠的,不违反上面的第一条规则,但依然是有问题的:
AcWing 291. 蒙德里安的梦想

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

那该如何避免这个问题呢?其实就是“尝试”两个状态叠加后再判断目标状态是不是满足连续奇数个\(0\)

四、实现代码

#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]; //动态规划的状态数组
vector<int> state[M];//对于每个状态而言,能转转移到它的状态有哪些,预处理一下(二维数组)
int st[M];  //某种状态是否合法,就是是不是存在奇数个连续0

int main() {
    //优化输入
    ios::sync_with_stdio(false);
    while (cin >> n >> m, n || m) {
        //预处理1:计算每个二进制描述态是否合法,有奇数个连续0非法
        for (int i = 0; i < 1 << n; i++) {
            int cnt = 0;    //连续0的个数
            st[i] = 1;      //默认此态是合法的
            for (int j = 0; j < n; j++)//遍历此状态的每一个二进制位
                if (i >> j & 1) {      //如果本位是1
                    if (cnt & 1) {
                        st[i] = 0;//此时,连续0发生了中断,需要判断是不是奇数个连续0
                        break;
                    }
                    //重新开始计数
                    cnt = 0;
                } else cnt++;//连续0个数++
            //最后一个cnt++后,依然可能有连续奇数个0
            if (cnt & 1) st[i] = 0;
        }
        //预处理2:枚举每个状态而言,它可能是从哪些有效状态转化而来
        for (int i = 0; i < 1 << n; i++) {
            //多组数据,每次预处理时清空一下
            state[i].clear();
            //状态i中以从哪些状态转化而来?
            for (int j = 0; j < 1 << n; j++)                
                if ((i & j) == 0 && st[i | j])
                    state[i].push_back(j);
        }
        //多组数据,每次清零
        memset(f, 0, sizeof f);
        //动态规划
        f[0][0] = 1;//出发点只有一种方案
        //遍历每一列
        for (int i = 1; i <= m; i++)
            //遍历第i列的所有状态j
            for (int j = 0; j < 1 << n; j++)
                //遍历第i-1列的所有状态k
                for (auto k: state[j])
                    f[i][j] += f[i - 1][k];
        //输出结果
        cout << f[m][0] << endl;
    }
    return 0;
}

五、代码解读

1、预处理非奇数连续零状态

#include <bits/stdc++.h>

using namespace std;
const int N = 12;
int st[N];
int n = 5;

int main() {
    //计算每个二进制描述态是否合法,有奇数个连续0非法
    for (int i = 0; i < 1 << n; i++) {
        int cnt = 0;    //连续0的个数
        st[i] = 1;      //默认此态是合法的
        for (int j = 0; j < n; j++)//遍历此状态的每一个二进制位
            if (i >> j & 1) {      //如果本位是1
                if (cnt % 2) st[i] = 0;//此时,连续0发生了中断,需要判断是不是奇数个连续0
                //重头计数
                cnt = 0;
                //本状态判断完毕,已经判断为非法状态,不必继续
                continue;
            } else cnt++;//连续0个数++

        //最后一段二进制位0的个数是否为奇数
        if (cnt % 2) st[i] = 0;

        //输出每个状态是否合法
        for (int j = 0; j < n; j++) {
            printf("%d ", i >> j & 1);
        }
        printf(" 是否合法:%d\n", st[i]);
    }
    return 0;
}

2、预处理状态之间转化关系

(1) i & j ==0 同一列的每一行都不能同时探出小方格,那样会有重叠。

(2) 解释一下st[i | j]
已经知道\(st[]\)数组表示的是这一列有没有连续奇数个\(0\)的情况,我们要考虑的是第\(i-1\)列(第\(i-1\)列是这里的主体)中从第\(i-2\)列横插过来的,还要考虑自己这一列(\(i-1\)列)横插到第\(i\)列的,比如 第\(i-2\)列插过来的是\(k=10101\),第\(i-1\)列插出去到第\(i\)列的是 \(j =01000\),那么合在第\(i-1\)列,到底有多少个\(1\)呢?自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101,最终的这个状态叠加和,是用来检查是不是存在奇数个\(0\)的期待状态值。

3、最终方案数

总共\(m\)列,我们假设列下标从\(0\)开始,即第\(0\)列,第\(1\)列……,第\(m-1\)列。根据状态表示\(f[i][j]\) 的定义,我们答案是什么呢? 请读者返回定义处思考一下。答案是\(f[m][0]\), 意思是前\(m-1\)列全部摆好,且从第\(m-1\)列到\(m\)列状态是\(0\)(意即从第\(m-1\)列到第\(m\)列没有伸出来的)的所有方案,即整个棋盘全部摆好的方案。

相关文章:

  • 2021-08-22
  • 2021-12-05
  • 2021-12-05
  • 2021-06-27
  • 2021-12-17
  • 2021-04-16
  • 2022-01-23
猜你喜欢
  • 2021-06-15
  • 2022-01-27
  • 2022-12-23
  • 2021-12-27
  • 2022-12-23
  • 2021-08-17
  • 2022-12-23
相关资源
相似解决方案