21.2.22 T2 多米诺骨牌 真·详解

题面

Description

有一个 r ∗ c r * c rc 的矩形,和一些 1 ∗ 2 1*2 12 的多米诺骨牌。如果想用这些骨牌刚好填满这个矩
形,使得没有位置是空出来的,多米诺骨牌也没有重叠。请问有多少种方法刚好填
满这个矩形呢?一种可能的填法如下图:
在这里插入图片描述
设定矩形是有方向的,旋转之后相同和相互对称的填法应当计算为不同的填法。

Input

一行两个数 r , c r,c r,c。表示矩阵的列数和行数。

Output

一个整数,表示填法总数。

Sample Input

2 11

Sample Output

144

数据范围与约定

20% 数据满足 r , c ≤ 5 r,c≤5 r,c5
100% 数据满足 r , c ≤ 11 r,c≤11 r,c11

题解

观察数据范围,很容易想到状压dp,那在这道题中 0 0 0 1 1 1 究竟表示什么呢?横竖?仔细想想这并不好对应二进制数的数位,那应该对应什么呢?

参考自李煜东的解析

观察每行的骨牌,我们会发现只有竖着的上面半截对下一行的排布会造成影响。那我们就可以用二进制数的每一位对应每一列的排布情况,用 1 1 1 表示该列是对应的方块是竖着的骨牌的上一半,用 0 0 0 表示其它情况。那么 f [ i ] [ j ] f[i][j] f[i][j]表示的则是第 i i i 行的分布情况如 j j j 时,前 i i i 行总的方案数,其中 j j j 是一个十进制数。
从上一行往下一行转移时,上一行则应满足这些条件:

  1. j j j & k k k 的值为 0 0 0,也就是说,上一行与当前行不能有两个同一列的方块都是某个竖着的骨牌的上一半,这很好理解吧,如果这样的话,就与 f [ i ] [ j ] f[i][j] f[i][j] 的定义矛盾了。
  2. j j j | k k k 的二进制数中连续的 0 0 0 的数量必须是偶数。 j j j | k k k 的二进制数什么时候会出现 0 0 0 的情况呢?必须是 j j j k k k 的那一位都是 0 0 0 ,那这种情况对应的在摆骨牌是是什么样呢?很显然,是上一行与当前这一行都是横着的骨牌时。因为骨牌长为 2 2 2 所以连续的 0 0 0 的数量也就是横着的骨牌的总长度应该为偶数。那为什么不能是竖着的呢?因为如果是竖着的话,那么上一行的这一列对应的应该是 1 1 1 ,执行或运算后得到的值为 1 1 1,则不是 0 0 0 了,看到这里应该明白了吧,我写到这里时,也才终于想明白,dp真的要自己努力去想,才会一点点的进步。

而满足 j j j | k k k 的二进制数中连续的 0 0 0 的数量是偶数的数我们可以先预处理出来。

代码如下

for(int i = 0; i < 1 << m; i++) {
		bool cnt = 0,has_odd = 0;
		for(int  j = 0; j < m; j++) 
			if(i >> j & 1) has_odd |= cnt,cnt = 0;
			else cnt ^= 1;
		in_s[i] = has_odd | cnt ? 0 : 1;
	}

我在理解这个代码的时候想了很久才想明白,仔细观察能够发现在第一次(每次重复中连续执行 e l s e else else 的第一次)执行 e l s e else else 操作时 c n t cnt cnt 的值只会是 0 0 0,因为此时的 c n t cnt cnt 要么是 i i i 第一位就是 0 0 0,要么是在执行了 i f if if 操作之后,而每次 i f if if 都会把 c n t cnt cnt 更新为 0 0 0 。如果此时出现一段连续的 0 0 0 c n t cnt cnt 则会不断与 1 1 1 异或,也就是在 0 0 0 1 1 1 之间不断转换,那么如果 0 0 0 的个数是奇数时,最终 c n t cnt cnt 的值就应该是 1 1 1 。而 h a s o d d has_odd hasodd 则会被或运算更新为 1 1 1 而当 h a s o d d has_odd hasodd 被更新为 1 1 1 后它的值就再也不会变化了。不禁感叹位运算的奇妙! 注意我们这里要把 i i i 对应的二进制数中前面的 0 0 0 也给算进去,因为一共有 m m m 列啊,前面的补进去的 0 0 0 也表示着分布情况。所以最后会有当奇数个 0 0 0 出现之后前面没有 1 1 1 的情况。那么我们 h a s o d d has_odd hasodd c n t cnt cnt 中只要有一个为 1 1 1,那么 i i i 就是不合法的。
呼~自己终于彻底想明白了。

完整代码如下

#include<cstdio>
int n,m;
long long f[12][1 << 11];
bool in_s[1 << 11];
int main() {
	scanf("%d%d",&m,&n);
	for(int i = 0; i < 1 << m; i++) {
		bool cnt = 0,has_odd = 0;
		for(int  j = 0; j < m; j++) 
			if(i >> j & 1) has_odd |= cnt,cnt = 0;
			else cnt ^= 1;
		in_s[i] = has_odd | cnt ? 0 : 1;
	}
	f[0][0] = 1;
	for(int i = 1; i <= n; i++)
		for(int j = 0; j < 1 << m; j++) {
			f[i][j] = 0;
			for(int k = 0;k < 1 << m; k++)
				if((j & k) == 0 && in_s[j | k])
					f[i][j] += f[i - 1][k];
		}
	printf("%lld",f[n][0]);
	return 0;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值