AcWing 291. 蒙德里安的梦想

题目传送门

题目描述

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

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

如下图所示:

2411_1.jpg

输入格式

输入包含多组测试用例。

每组测试用例占一行,包含两个整数 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

算法求解

分析

先把所有能横着的小长方块放上,剩下的竖着的小长方块就只有一种放法

所以总的方案数 = 放横着小长方块的数目

f[i][j]表示:横着放小长方块第i列处于状态j的时候的总的方案数

  • \(j==0\)表示:第i-1列没有小长方块捅到第i列
  • \(j== (10010)_2\),表示第i-1列的第1、4行有横着的小长方块捅到第i

要使得第i列能放横着的小长方块,并且放完小长方块后,i-1还能竖着放小长方块,使用j表示放完小长方块后第i列的状态,k表示放完后第i-1列的状态。第那么要求两点,

  1. i列和第i-1列放的横小长方块不能冲突:j&k == 0
  2. 放完第i列的小长方块后,第i-1列连续0的数量不能是奇数:st[j&k] == true

其中每个状态是否合法,可以预处理。


然后是dp过程

注意f[0][0] = 1 表示第0列没有格子捅出来的情况只有一种(什么都不放)

返回的时候返回f[m][0](其实一共只有 0~m-1列),表示f[m][0]第m列没有格子捅出来的总情况有多少种。

代码

#include<iostream>
#include<cstdio>
#include<cstring> 
#include<algorithm>
using namespace std;
typedef long long LL;

const int N = 12; // 一共多少列 
const int M = 1 << N; //每列的状态数
LL f[N][M]; // f[i][j]表示当第i列处于状态j的时候的状态数
bool st[M];  // 表示某个状态是否合法,当有连续奇数个0的时候不合法
int n, m;
int main()
{
	while(1)
	{
		scanf("%d%d", &n, &m);
		if(n == 0 && m == 0) break;
		
		// 标记所有合法/不合法状态
		 for(int i = 0; i < 1 << n; i++)
		 {
		 	// i 表示一个状态
			int cnt = 0;
			st[i] = true; // 假定其合法 
			// 看i的每一位
		 	for(int j = 0; j < n; j++)
		 	{
		 		// 如果i的第j位为1的话 
		 		if(i>>j & 1)
				{
					// 前面有奇数个0 
				 	if(cnt & 1)
				 	{
				 		st[i] = false;
						break;	
					}
					cnt = 0; // 遇到1了,前面0的数量清0 
			    }	
			    else cnt++;
			}
			if(cnt & 1) st[i] = false; // 判断一下最后一段是不是奇数个0 
		 }
		 
		 memset(f, 0, sizeof f);
		 f[0][0] = 1; // 第0列没有横着的长方块捅出来的方案数为1
		 
		 for(int i = 1; i <= m; i++) // 枚举所有的列 
		 {
		 	for(int j = 0; j < 1 << n; j++) // 枚举第i列所有状态 
			{
			 	for(int k = 0; k < 1 << n; k++) // 枚举第i-1列所有的状态 
				{
					// j&k=0表示的是第i列和第i-1列没有冲突的行
					// j|k的二进制表示将第i列放了横着的长方块后,第i-1列有多少个1 
				 	if(((j&k) == 0) && st[j|k]) // 注意加括号,有优先级 
					{
						f[i][j] += f[i-1][k]; 	
					} 
			    }	
			}	
		} 
		cout << f[m][0] << endl;
	}
	return 0; 
} 

时间复杂度

参考文章

  1. 所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩。
  2. 本题等价于找到所有横放 1 X 2 小方格的方案数,因为所有横放确定了,那么竖放方案是唯一的。
  3. 用f[i][j]记录第i列第j个状态。j状态位等于1表示上一列有横放格子,本列有格子捅出来。转移方程很简单,本列的每一个状态都由上列所有“合法”状态转移过来f[i][j] += f[i - 1][k]
  4. 两个转移条件: i 列和 i - 1列同一行不同时捅出来; 本列捅出来的状态j和上列捅出来的状态k求或,得到上列是否为奇数空行状态,奇数空行不转移。
  5. 初始化条件f[0][0] = 1,第0列只能是状态0,无任何格子捅出来。返回f[m][0]。第m + 1列不能有东西捅出来。
#include<bits/stdc++.h>
using namespace std;
const int N = 12, M = 1 << N;
int st[M];
long long f[N][M];


int main(){
    int n, m;
    while (cin >> n >> m && (n || m)){

        for (int i = 0; i < 1 << n; i ++){
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++)
                if (i >> j & 1){
                    if (cnt & 1) st[i] = false; // cnt 为当前已经存在多少个连续的0
                    cnt = 0;
                }
                else cnt ++;
            if (cnt & 1) st[i] = false; // 扫完后要判断一下最后一段有多少个连续的0
        }

        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 ++)
                    if ((j & k) == 0 && (st[j | k])) 
                    // j & k == 0 表示 i 列和 i - 1列同一行不同时捅出来
                    // st[j | k] == 1 表示 在 i 列状态 j, i - 1 列状态 k 的情况下是合法的.
                        f[i][j] += f[i - 1][k];      
        cout << f[m][0] << endl;
    }
    return 0;
}

作者:sjytker
链接:https://www.acwing.com/solution/content/5121/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:sjytker
链接:https://www.acwing.com/solution/content/5121/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

posted @ 2022-03-02 00:01  VanHope  阅读(72)  评论(0编辑  收藏  举报