[题解]UVA11270 Tiling Dominoes

Clu3ter

2020-07-21 13:58:27

Solution

[![](https://cdn.luogu.com.cn/upload/image_hosting/c988ay6n.png)](https://www.luogu.com.cn/problem/UVA11270) ------------ 看到了很多dalao发的题解,但是理解得不是特别清楚,研究了一下午终于懂了(:D) 这里我们**逐格**进行DP。但记录的状态是各行内的**轮廓线**上格子的覆盖情况。 $f[i][s]$表示第i行的轮廓线状态为s时的方案数。 轮廓线……是啥? 请看下图 ![](https://cdn.luogu.com.cn/upload/image_hosting/4bwrk3uw.png) 图中绿色格子为$\color{LimeGreen}\text{当前}$推到的格子。那条金黄色亮闪闪的线就是$\color{Goldenrod}\text{轮廓线}$。 我们用格子所在的列数 来给轮廓线上方的格子(白色格子)编号。编完号后即可二进制**状态压缩**,用1表示被覆盖,用0表示未被覆盖。 如图: (由于数字的一般为从高到低,便于与图示对应我们这里采用“逆写法”) ![](https://cdn.luogu.com.cn/upload/image_hosting/1xjpbszv.png) 按照题目的设定,要 _覆盖整个网格_ ,**轮廓线之上的所有格子都应该是1**,**但**!**是**!由于考虑到有**竖放**的骨牌,我们使与轮廓线相邻的这些方格(白色)中能够存在0,起到一个类似“缓冲?”的作用。$\color{red}\text{<重点1>}$(具体的意思可以看下文) 好,状态表示完毕,接下来我们来看转移的方法 显然,对于当前格(绿),只有3种决策: >1.竖放(向上) > >2.横放(向左) > >3.留空 **设白色格内的状态为$k$,转移后为$k'$** **设绿色格所在列数为$j$** **仅第$x$位为1的状态为$p[x]\quad$** (即1<<(x-1)) ![](https://cdn.luogu.com.cn/upload/image_hosting/es7g8u60.png) 我们来分类讨论: ### 1.竖放: 什么时候需要竖放呢?$\quad$应该观察当前格**上方**的格子状态。 当前格上方的状态用数据表示即为$k \& p[j]$ - 如果当前格上方为**0**,那么**必须放一个竖放的骨牌** 为什么呢?因为上方白色格子的内容是**已经确定**的,也就是说,此时不放一块向上的骨牌,那么上方这个格子就会**永远无法被覆盖**,这与题目要求的**覆盖整个网格**不符。因此必须放一个竖向骨牌$\color{red}\text{<重点2>}$ - 那么当前格上方为**1**,那么你肯定**无法**放一个向上的骨牌。 **综上**,若上方为0则放置竖向骨牌(!(k&p[j])) 同时,当前格也**不能位于第一行**。(j>1) 接下来我们来整个转移方程出来。 上图 ![UIzAht.gif](https://s1.ax1x.com/2020/07/21/UIzAht.gif) 在经过转移后,我们的动规推进了一格,**k的第j位在推进前后,从当前格的上一个格子,变成了当前格本身(观察左下角的格子排列)**(理解这一点很重要(至少对于我自己来说))$\color{red}\text{<重点3>}$ 即k的第j位 从0变成了1,而原来的上方格子则不用再考虑了 >#或许你可以停下来思考一下…… 由于求的是总方案数,因此转移时采用**加和**,初始化为0。 因此,状态转移方程如下: $${\text{if}((i>1)\, \&\&\,!(k\&p[j]))}\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad$$ $$ k'=k|p[j]$$ $$ f[i][k']+=f[i-1][k] $$ ### 2.横放: 现在有了第一种的经验,我们可以依样画葫芦,轻松地推出其他的情况 >#试着自己推一推? 情况1中我们已经得知,**如果上方的格子未被覆盖,则必定要放一个竖向骨牌将其填满**(重点2处)。即下文条件2. 横放的条件是 1. 左侧格子没有被覆盖(!(k&q[j-1])) 2. 上方的格子已经被覆盖(k&q[j]) 3. 不在第一列(j>1) 而具体的转移方法 则是 横放后$k'$的第$j$位(本身)与第$j-1$位(左侧格子)都应该是**1**。而由条件2可知原k的第$j$位原来就是**1**,故只需要将左侧格子改为1。 状态转移方程: $${\text{if((j>1) \&\& !(k\&q[j-1]) \&\& (k\&q[j]))}}\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad$$ $$ k'=k|p[j-1]$$ $$ f[i][k']+=f[i-1][k] $$ ### 3.留空: 同情况2条件2,只有在上方格子被覆盖的情况下,才可以进行除了竖放以外的操作,而对于留空,则没有什么要求。 则条件是: 1. 上方的格子已经被覆盖(k&q[j]) 转移方法则是$k'$的第$j$位改为0; $${\text{if(k\&q[j])}}\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad$$ $$ k'=k\; xor\; p[j-1]$$ $$ f[i][k']+=f[i-1][k] $$ >#思考 留空 与 竖放 的关系 #### 可以结合下面这张图来理解。 ![](https://cdn.luogu.com.cn/upload/image_hosting/kn2k4u5o.png) 初始化为f[0][(1<<m)-1]=1; (把第0行填满) 最终答案为f[d][(1<<m)-1]; (最后一行填满) ### 一些优化 - 由于每一行的状态仅有上一行推出,且最终只取最后一行,可以使用2行的**滚动数组**优化; - 预处理p数组(即1<<(i-1)); - 防止**状态压缩**超出整数范围,始终保证n>m,使m≤10; ### 代码 ```cpp #include<bits/stdc++.h> using namespace std; int n,m; long long int f[2][(1<<10)]; int p[20]; int main(){ for(int i=1;i<=15;i++){ p[i]=1<<(i-1); }//预处理 while(~scanf("%d %d",&n,&m)) { if(n<m) swap(n,m);//交换,使m不大于10 int d=0;//滚动数组下标 memset(f,0,sizeof(f));//清空 f[0][(1<<m)-1]=1;//初始化 for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ d^=1;//d在0与1之间滚动 memset(f[d],0,sizeof(f[d]));//清空 for(int k=0;k<(1<<m);k++){ if(k&p[j]) f[d][k^p[j]]+=f[d^1][k];//留空 if((j>1) && !(k&p[j-1]) && (k&p[j]))//横放 f[d][k|p[j-1]]+=f[d^1][k]; if((i>1) && !(k&p[j])) f[d][k|p[j]]+=f[d^1][k];//竖放 } } } cout<<f[d][(1<<m)-1]<<endl;//最终输出最后一行也填满的情况 } } ```