状态压缩、记忆化搜索---蒙德里安的梦想

291. 蒙德里安的梦想 - AcWing题库

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

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

如下图所示:

2411_1.jpg

输入格式

输入包含多组测试用例。

每组测试用例占一行,包含两个整数 NN 和 MM。

当输入用例 N=0,M=0N=0,M=0 时,表示输入终止,且该用例无需处理。

输出格式

每个测试用例输出一个结果,每个结果占一行。

数据范围

1≤N,M≤111≤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的长方形。通过状态压缩的思想,我们将棋盘的每一列进行压缩,用位运算表示每一列的状态,然后通过动态规划(DP)来实现转移。

题目分析

  • 棋盘的状态压缩:由于棋盘是二维的,我们需要解决的是如何摆放这些1×2的长方形。在每列上我们可以通过一个二进制数表示列的状态。每一位(bit)代表这一列中的每一个格子,当这一位为1时表示该格子已被占用,为0表示未被占用。

  • 列与列之间的状态转移:由于每一列的状态与相邻列的状态有关,因此需要考虑相邻列之间的“兼容性”,也就是确保相邻的列不会发生重叠。例如,当前列与前一列不能有相邻的空格被两个不同的长方形占据,否则就不合法。

状态压缩的应用

  1. 状态表示

    • 使用一个 n 位的二进制数 i 来表示一列中的每个格子的状态。比如,对于一个 2×4 的棋盘,可以使用 2 位二进制数表示每一列的状态:

      • 00 表示这一列的两个格子都没有被占据;

      • 01 表示这一列的上面格子已经被占据,下面的格子没有被占据;

      • 10 表示这一列的上面格子没有被占据,下面的格子被占据;

      • 11 表示这一列的两个格子都已经被占据。

  2. 合法状态的判断

    • 每列的状态需要满足1×2的长方形能够合理地摆放。这意味着,在同一列中,连续的空格数量必须为偶数,因为1×2的长方形需要覆盖两个格子。如果有奇数个空格,则该列的状态为非法状态。

  3. 相邻列的状态转移

    • 在进行状态转移时,需要确保当前列的状态和前一列的状态是合法的。具体来说,当前列的状态 i 和前一列的状态 j 不应在同一个位置上都有1,即 (i & j) == 0。此外,ij 的合并状态必须是合法的(即满足所有被占用的格子组成的空隙的数量为偶数)。

  4. 状态转移方程

    • 定义 f[i][j] 表示前 i 列的状态为 j 的方案数。那么状态转移方程为:

      f[i][j] += f[i - 1][k]  // 对于所有合法的 k

    • 其中,k 是前一列的状态,state[j][k] == 1 表示状态 jk 之间可以合法转移。

代码分析

  • 状态初始化

    • state[i][j] 用于表示列状态 ij 是否可以组合为合法状态,st[i] 用于表示单列状态 i 是否合法。

  • DP 过程

    • 通过动态规划数组 f,递推计算出所有可能的合法分割方案。

  • 状态压缩的意义

    • 通过位运算,我们将二维的棋盘问题转化为一维的状态表示,并且通过位操作来判断状态的合法性和相邻列状态的转移。这种做法可以极大地减少我们对状态的存储和处理需求,避免暴力解法中的重复计算。

状态压缩的步骤总结:

  1. 列状态的表示:每列的状态用二进制表示,位数等于行数 N

  2. 列状态的合法性判断:每列必须能被1×2的长方形完全覆盖,空格数必须为偶数。

  3. 状态转移的合法性判断:相邻列的状态不能冲突,且合并后的状态必须是合法的。

  4. 通过DP递推解决:使用 f[i][j] 表示前 i 列状态为 j 的方案数,最终输出结果 f[m][0] 即为棋盘的合法分割方案数。

小结

通过状态压缩,我们将棋盘列的排列组合转化为位运算操作,压缩了存储和计算量,同时动态规划利用状态转移方程来高效地计算出分割方案的数量。 代码from栗子ing

import java.util.*;
​
public class Main{
    public static void main(String[] args) {
        Scanner sca = new Scanner(System.in);
        int N = 12, M = 1 << N;
        long[][] f = new long[N][M];
        int[][] state = new int[M][M];
        boolean[] st = new boolean[M];
        
        while (true) {
            int n = sca.nextInt();
            int m = sca.nextInt();
            //当 n 和 m 同时为 0 结束循环
            if (n == 0 && m == 0) {
                break;
            }
            
            for (int i = 0; i < 1<< n; i ++ ) {
                int cnt = 0;    //  表示的是当前 前面0的个数
                boolean flag = true;
                for (int j = 0; j < n; j ++ ) { // 从上倒下判断有多少个0
                    // 判断现在这位是不是 1
                    if ((i >> j & 1) == 1 ) {
                        //如果是1,判断1前面0的个数是不是偶数,奇数的话就结束
                        if ((cnt & 1) == 1) { // & 1  等于 1 就是奇数,反之是偶数
                            flag = false;
                            break;
                        }
                        cnt = 0;
                    } else {
                        cnt++; // 如果当前不是1 ,则 0 的个数 +1
                    }
                }
                // 最后还要判断一下最后一层0的个数是不是奇数
                if ((cnt & 1) == 1) flag = false;
                // 最后将这一种状态存入st数组,表示true 合法 或者false非法
                st[i] = flag;
            }
            
            // 这是 i- 1 到 i列的方块
            for (int i = 0; i < 1 << n; i ++ ) {
                // 将所有的状态清零,因为多组数据防止上一组的影响
                Arrays.fill(state[i], 0); 
                
                for(int j = 0; j < 1 << n; j ++ ) {
                    // 当满足 1. i 和 j没有相交(同一行的两列不能连续放置方块会重叠)
                    //        2. i - 1 列的空格数是不是偶数
                    if ((i & j) == 0 && st[i | j]) {
                        state[i][j] = 1;
                    }
                }
            }
            
            for (int i = 0; i < N; i ++ ) {
                // 因为有多组数据,防止上一组数据的干扰
                Arrays.fill(f[i], 0);
            }
            // 边界,横着在第一列方只有一种方案就是 什么也不放 
            f[0][0] = 1;
            
            // 最后的 DP部分
            //为什么从1开始呢,因为从0开始的话,我们定义的f[m][j]就是前i - 1列已经摆好
            //如果是0开始,就会从-1个开始摆好,因为我们没有-1列,所以从1开始
            for (int i = 1; i <= m ; i ++ ) {
                // 枚举 i - 1 到 i 的所有方案啊
                for (int j = 0; j < 1 << n; j ++) {
                    //枚举 i- 2 到 i- 1 的所有方案啊
                    for (int k = 0; k < 1 << n; k ++ ) {
                        // 现在的方案等于前面每种k方案的总和
                        if (state[j][k] == 1 ) {
                            f[i][j] += f[i - 1][k];
                        }
                    }
                }
            }
            
            System.out.println(f[m][0]);
        }
    }
}

 

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值