Java/C++---状态压缩dp---蒙德里安的梦想(每日一道算法2022.10.22)

三个小时的恶战,怎么这两天的题这么难啊

题目:
求把 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/
本文仅用作学习记录和交流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值