状态压缩动态规划:蒙德里安的梦想

本文介绍了一种使用状态压缩动态规划(状压DP)解决棋盘分割问题的方法。通过将N×M的棋盘分割成1×2的矩形,探讨如何计算不同的分割方案数量。文章详细解释了状态表示、状态计算及初始状态,并给出了具体的时间复杂度分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

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

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

如下图所示:
在这里插入图片描述
输入格式
输入包含多组测试用例。

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

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

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

数据范围
1 ≤ N , M ≤ 11 1≤N,M≤11 1N,M11

输入样例

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

算法思想(状压DP

题目求把 N × M N×M N×M 的棋盘分割成 1 × 2 1×2 1×2的长方形的方案数。分析可知,只要确定了 1 × 2 1×2 1×2 小方块的横向摆放方式,纵向小方块的摆放方式也就确定了,有且只有惟一种对应方式。这样,题目可以转换成求 1 × 2 1 × 2 1×2 小方块的横向摆放的方案数。

状态表示

f[i][j]表示已经完成前 i 列,在其上一列(即i - 1列)伸出来的横向小方块状态为j的情况下,横向摆放小方块的方案数。

j是上一列小方块的状态,其二进制的每一位表示是否有伸出的横向小方块,例如有 5 5 5行方格,当 j = ( 01001 ) 2 j = (01001)_2 j=(01001)2 时,表示上一列(第2列)中,第 0 0 0行、第 3 3 3行伸出了小方块,如下图所示:
请添加图片描述

状态计算

计算f[i][j],判断第 i - 1 列状态能够转移过来的条件:

  1. i - 1列伸出的状态k与当前列的状态j不能冲突(相同行不能同时为1),即j & k == 0,否则无法摆放。如下图所示: k = ( 00001 ) 2 k = (00001)_2 k=(00001)2 j = ( 01001 ) 2 j = (01001)_2 j=(01001)2存在冲突。
    在这里插入图片描述

  2. 在当前列摆放了横向小木块后,所有剩余的空白方格能够被竖向的小木块填满。那么在每一列所有连续的空白方格的个数必须是偶数,否则无法摆放竖向的小木块,即k | j不能存在连续奇数0。如上图所示,k|j = ( 01001 ) 2 =(01001)_2 =(01001)2,有奇数个0,表示 3 ∼ 4 3\sim4 34行之间只有 1 1 1行,无法摆放竖向的小木块。

如果 j j j k k k满足上述两个条件,那么状态转移方程:
f [ i ] [ j ] = ∑ f [ i − 1 ] [ k ] f[i][j] = \sum f[i - 1][k] f[i][j]=f[i1][k]

初始状态

f[0][0] = 1表示第0列在其上一列没有伸出任何小方块格的情况下,横向摆放小方块的方案数为1(即不放置横向小方块)。

时间复杂度

  • 状态数量: n × 2 n = 11 × 2 11 n\times2^n=11 \times 2^{11} n×2n=11×211
  • 状态转移需要枚举的数量: 2 n = 2 11 2^n = 2 ^{11} 2n=211

时间复杂度: O ( 4 × 1 0 7 ) O(4\times10^7) O(4×107)

代码实现

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << N;
/*
f[i][j]表示已经完成前 i 列,在其上一列(即i - 1列)
伸出来的横向小方块状态为j的情况下,横向摆放小方块的方案数
*/
LL f[N][M];
//st[i]表示数i的二进制表示中是否不存在连续奇数个0
bool st[M];
int main()
{
    int n, m;
    while(cin >> n >> m, n || m)
    {
        //预处理st,计算所有列的状态为i时,是否不存在连续奇数个0
        for(int i = 0; i < 1 << n; i ++)
        {
            int cnt = 0; //统计0的个数
            st[i] = true; 
            for(int j = 0; j < n; j ++)
            {
                if((i >> j & 1) == 0) cnt ++;
                else { //当前位是1
                    //如果cnt为奇数
                    if(cnt & 1) 
                    {
                        st[i] = false;
                        break;
                    }
                    cnt = 0; //重新统计0的个数
                }
            }
            //如果最后剩余0的个数为奇数
            if(cnt & 1) st[i] = false;
        }
        //初始状态
        memset(f, 0, sizeof f);
        //初始状态,表示第0列在其上一列没有伸出任何小方块格的情况下,
        //横向摆放小方块的方案数为1(即不放置横向小方块)
        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 ++)
                {
                    //满足状态转移条件,不冲突,并且不存在连续奇数个1
                    if((k & j) == 0 && st[k | j])
                        f[i][j] += f[i - 1][k];
                }
            }
        }
        
        //结果方案数为完成前m列,在每一列不伸出任何小方块的状态下的值
        cout << f[m][0] << endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值