【状压dp】方格填充问题

题目描述

对于一个行数为n,列数为m的方格网盘,用若干个2行1列的长方形去填充它,问一共有多少种方案?

输入描述

第一行为 n n n ( 1 ≤ n 1\leq n 1n ≤ 11 \leq 11 11), m m m ( 1 ≤ m 1\leq m 1m ≤ 11 \leq 11 11)

输出描述

输出方案数

样例

input
2 3
output
3


分析

本题采用的方法为状压dp,用0和1来表示网格中小矩形的放置情况。

小矩形有两种放置情况:横着放和竖着放。如果是是竖放,那么把上一格记为1,下一格记为0;如果是横放,那么把两个方格都记为0,如下所示:

在这里插入图片描述
在这里插入图片描述
于是,对于某一种放置方法,可以用 n ∗ m n * m nm 大小的唯一的 0 - 1 序列来表示,例如如下放置情况与其对应的序列为:
在这里插入图片描述
很自然可以想到,对于 n n n 行格子,枚举每一行格子的 0 - 1 序列,然后筛选出其中合法的序列,便可以统计出合法的情况了。

考虑到小矩形为2行1列的,当前行的放置是否合法,只与上一行的放置有关。即,如果让一行有1,那么该1的下面不能再放1,只能放0。这符合动态规划的“后无效性”原则,于是我们考虑使用动态规划来解这道题。

根据上述,我们提炼出了判断当前放置合法的两种情况:
①对于横放的矩形,连续0的数目必须是偶数;
②对于竖放的矩形,上一行如果是1,那么该1的下面只能是0;上一行如果是0,那么该0的下面只能是1。换句话说,相邻的上下两个不能都是1。

对于 n n n m m m 列的网格,第 i i i 行有 m m m 个格子,也就是有 2 m 2^m 2m 种状态,用 0 ~ 2m-1 来表示这些状态,例如对于3列的网格,每一行的状态可以有000, 001, 010, 011, 100, 101, 110, 111 八种。

根据前文可以知道,当前行的状态仅受上一行状态的影响。首先枚举第 i i i 行的所有状态,然后再枚举第 i − 1 i - 1 i1 行的所有状态,在这两行所有的组合状态中,筛选出所有可行的组合。

对于验证后可行的组合为 s t a t e i state_i statei s t a t e i − 1 state_{i-1} statei1 ,它们都是一个二进制串。例如, s t a t e i state_i statei = 111 与 s t a t e i − 1 state_{i-1} statei1 = 000 是兼容的,也可以说 s t a t e i state_{i} statei = 7 与 s t a t e i − 1 state_{i-1} statei1 = 0 是兼容的。我们用一个大小为 n × 2 m n × 2^m n×2m d p dp dp 数组来记录当前访问的第 i i i 行的状态。例如此状态下,记录 d p [ i ] [ 7 ] + = d p [ i − 1 ] [ 0 ] dp[i][7] += dp[i - 1][0] dp[i][7]+=dp[i1][0] ,它表示,当前行状态7的可放置情况等于当前放置数累加上上一行状态0的可放置情况总数。

由于动态规划是从前向后顺序更新的,所以访问第 i i i 行时,上一行所有状态的可放置数已经是确定的了。 只需要考虑当前行可行的状态,然后找到与该状态兼容的所有状态,将所有兼容的状态累加到当前状态下,便可得到当前 d p [ i ] [ j ] dp[i][j] dp[i][j] 的大小了。

题解

①判断第 i i i 行与第 i − 1 i - 1 i1 行是否兼容。

根据前文,第 i i i 行与第 i − 1 i - 1 i1 行如果兼容,需要满足两个情况,首先,第 i i i 行与第 i − 1 i - 1 i1 行上下不能有两个连续的1;其次,每一行连续0的个数必须是偶数。记当前行的状态为 j j j ,上一行的状态为 k k k ,对于第一点,需要保证: k k k & j j j == 0 0 0

以下重要:

对于第二点,需要保证: k k k | j j j 中连续0的个数是偶数,这样做位运算的原因是把竖放的矩形给排除掉,剩下的0一定是第 i i i 行中横放的0。例如, j j j = 0001, k = k = k= 0000,第 i i i 行前3个0不可能是来自于上一行或者下一行,只能来自于本行,而本行横放的0必然应该是偶数个。

于是,我们考虑把第 i i i 行与第 i − 1 i - 1 i1 行做或运算后,没有奇数个连续0的兼容情况放到数组中。例如,对于4列的网格,其所有兼容情况为:

在这里插入图片描述
只要保证第 i i i 行与第 i − 1 i - 1 i1 行没有上下连续的两个0,且其做或运算后符合上述情况中的一种,就认为它是可行的。参见如下代码:

if (legal[k | j] && ((k & j) == 0))
	dp[i][j] += dp[i - 1][k];  // 累加当前状态

而对于如何写这个判断一个二进制序列是否有连续奇数0的函数,只需要知道这样判断的原因和目的,不管怎样写起来都没有难度。我提供一个自己写的实现:

//对于m列,有[0, 2^m)个二进制序列,先把十进制数转二进制序列,再判断是否有连续奇数个0
	for (int i = 0; i < (1 << m); i++)
	{
		bool continuous_0_parity = 0;    //连续0的个数的奇偶性,0为偶,1为奇
		bool has_continuous_odd_0 = 0;   //记录是否有奇数个连续0
		legal[i] = 1;                    //暂时记录状态i是合法的
		for (int j = 0; j < m; j++)
		{
			if ((i >> j) & 1)   //出现1
			{
				if (continuous_0_parity == 1)   //有连续个级数0
					has_continuous_odd_0 |= continuous_0_parity;
				continuous_0_parity = 0;    //奇偶性置空,重新计算奇偶性
			}
			else     //出现0
				continuous_0_parity ^= 1;   //奇偶性取反
		}
		if (continuous_0_parity | has_continuous_odd_0)   //如果整段中出现了奇数个0
			legal[i] = 0;      //说明此状态是不合法的
		//否则就是合法的
	}

②遍历每一行,枚举当前行的所有状态 j j j ,对于状态 j j j 枚举上一行的所有状态 k k k ,如果 j j j k k k 是兼容的,就把 d p [ i ] [ j ] dp[i][j] dp[i][j] 进行累加。(如何判断它们合法,参见上文)

动态规划的初始值是 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0] = 1,表示第 0 0 0 行的全0状态肯定是合法的。遍历的框架大致如下:

for (int i = 1; i <= n; i++)     //遍历每一行
{
	for (int j = 0; j < (1 << m); j++)      //枚举当前行的所有状态
		for (int k = 0; k < (1 << m); k++)  //枚举之前行的所有状态
			if (/*状态 j 与状态 k 兼容*/)
			    //更新dp[i][j]的值
}

代码

本题的完整代码为:

/*
 * @Description: Mesh Filling Problem
 * @Author: Zhoujin - SDU
 * @email: 1761806916@qq.com
 * @Date: 2021-05-27 13:23:27
 * @LastEditTime: 2021-05-27 13:23:27
 * @FilePath: \week13\t4.cpp
 */
#include <iostream>
#include <algorithm>
#include <cstring>
#include <bitset>
#define ll long long 
using namespace std;
const int Max = 12;
ll dp[Max][1 << Max];
bool legal[1 << Max];
int n, m, cnt;

void show_dp()
{
	cout << "dp : " << endl;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 0; j < (1 << m); j++)
			cout << dp[i][j] << " ";
		cout << endl;
	}
}

void solve()
{
	//对于m列,有[0, 2^m)个二进制序列,先把十进制数转二进制序列,再判断是否有连续奇数个0
	for (int i = 0; i < (1 << m); i++)
	{
		bool continuous_0_parity = 0;    //连续0的个数的奇偶性,0为偶,1为奇
		bool has_continuous_odd_0 = 0;   //记录是否有奇数个连续0
		legal[i] = 1;                    //暂时记录状态i是合法的
		for (int j = 0; j < m; j++)
		{
			if ((i >> j) & 1)   //出现1
			{
				if (continuous_0_parity == 1)   //有连续个级数0
					has_continuous_odd_0 |= continuous_0_parity;
				continuous_0_parity = 0;    //奇偶性置空,重新计算奇偶性
			}
			else     //出现0
				continuous_0_parity ^= 1;   //奇偶性取反
		}
		if (continuous_0_parity | has_continuous_odd_0)   //如果整段中出现了奇数个0
			legal[i] = 0;      //说明此状态是不合法的
		//否则就是合法的
	}

	cout << "Legal cases: ";
	for (int i = 0; i < (1 << m); i++)
		if (legal[i])
			cout << bitset<sizeof(i)>(i) << " ";
	cout << endl << endl;
	dp[0][0] = 1;   //初始状态,第0行的全0状态肯定是合法的
	for (int i = 1; i <= n; i++)     //遍历每一行
	{
		cout << "row = " << i << endl;
		for (int j = 0; j < (1 << m); j++)      //枚举当前行的所有状态
		{
			dp[i][j] = 0;
			for (int k = 0; k < (1 << m); k++)  //枚举之前行的所有状态
			{
				if (legal[k | j] && ((k & j) == 0))
				{
					cout << "State i: " << bitset<sizeof(j)>(j) << endl;
					cout << "      k: " << bitset<sizeof(k)>(k) << endl;
					cout << "update dp[" << i << "][" << j << "] "
						<< "from " << dp[i][j];
					dp[i][j] += dp[i - 1][k];
					cout << " to " << dp[i][j] << endl;
				}
			}
		}
		cout << "row state : ";
		for (int j = 0; j < (1 << m); j++)
			cout << dp[i][j] << " ";
		cout << endl;
		cout << endl;
	}

	show_dp();
}

int main()
{
	cin >> n >> m;

	solve();
	if (n == 0 || m == 0)
		cout << 0 << endl;
	else
		cout << dp[n][0] << endl;

	return 0;
}

为了便于理解,我给dp的运行状态作出了一些中间输出,一边观察代码一边查看输出很有利于题目的理解,如下:

在这里插入图片描述
最后一行是正确答案。




其实还有一种复杂度为O(1)的方案,即先口算出 11 × 11 11×11 11×11 种可能的情况,然后根据输入写上输出就行了:

#include <iostream>
#define ll long long
using namespace std;

int main()
{
	ll res0[12] = { 0 };
	ll res1[12] = { 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0 };	
	ll res2[12] = { 0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 };	
	ll res3[12] = { 0, 0, 3, 0, 11, 0, 41, 0, 153, 0, 571, 0 };	
	ll res4[12] = { 0, 1, 5, 11, 36, 95, 281, 781, 2245, 6336, 18061, 51205 };	
	ll res5[12] = { 0, 0, 8, 0, 95, 0, 1183, 0, 14824, 0, 185921, 0 };	
	ll res6[12] = { 0, 1, 13, 41, 281, 1183, 6728, 31529, 167089, 817991, 4213133, 21001799 };	
	ll res7[12] = { 0, 0, 21, 0, 781, 0, 31529, 0, 1292697, 0, 53175517, 0 };		
	ll res8[12] = { 0, 1, 34, 153, 2245, 14824, 167089, 1292697, 12988816, 108435745, 1031151241, 8940739824 };	
	ll res9[12] = { 0, 0, 55, 0, 6336, 0, 817991, 0, 108435745, 0, 14479521761, 0 };	
	ll res10[12] = { 0, 1, 89, 571, 18061, 185921, 4213133, 53175517, 1031151241, 14479521761, 258584046368, 3852472573499 };	
	ll res11[12] = { 0, 0, 144, 0, 51205, 0, 21001799, 0, 8940739824, 0, 3852472573499, 0 };	
	ll* res[12] = {  res0, res1, res2, res3, res4, res5, res6, res7, res8, res9, res10, res11 };
	
	int n, m;
	cin >> n >> m;
	cout << res[n][m];
	
	return 0;
} 

这种方案样例跑起来都是 0ms,推荐试一试!!

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值