题解:P10975 Mondriaan's Dream

· · 题解

题目分析

1 \times 2 的小矩形填充一个大矩形,可以横着放或者竖着放,问方案数。

解题思路

可以观察到数据范围很小 N,M \le 11,可以考虑阶乘级算法或者指数级算法,那么考虑到要求记录方案数,尝试使用状态压缩。

设计状态

根据状态压缩的套路,把每一行当做一个“状态”。

再考虑每一行中每一列的方格归属情况:

  1. 可能与上一行的方格属于一个竖着放的小矩形,将这个状态设为 1
  2. 剩下的所有情况都把状态设为 0,比如连续两个位置安排一个横着的 1 \times 2 矩形,或者与下一行的方格属于一个竖着的矩形。

为什么这样安排呢?

因为我们是从上到下遍历,枚举到第 i 行的时候,需要考虑上一行对当前行的影响,第一种情况的时候,第 i-1 行对第 i 行的状态产生了影响。第二种情况的时候并没有产生影响。

综上所述,设 f_{i,s} 表示前 i 行,第 i 行的状态为 s 时的方案数。

### 状态转移 $$f_{i,j}= f_{i,j}+f_{i-1,k}$$ 转移条件。 - 状态 $j$ 和状态 $k$ 执行**按位与**运算得到的结果是 $0$。 这样就保证了每个数字 $1$ 的下方必须是 $0$,代表着补全竖着的 $1 \times 2$ 的矩形。 - 状态 $j$ 和状态 $k$ 执行**按位或**运算的二进制表示中,每一段连续的 $0$ 必须都是偶数个。 偶数个 $0$ 代表着横着的 $1 \times 2$ 的矩形。奇数个的话就无法满足了。 这两个条件保证了状态转移的合法性,对于条件一,直接计算就好了,对于条件二,可以提前预处理每一个状态是否合法,具体细节见代码。 ### 边界处理与输出答案 初始状态,$f_{0,0}=1$。 很明显,答案是 $f_{n,0}$,因为第 $n$ 行的时候,已经到了最后,不能再有 $1$ 状态了。 # AC CODE ``` #include<bits/stdc++.h> using namespace std; #define ll long long const int MOD = 1e9; const int N=13; ll f[N][1<<N]; //! 不开 long long 见祖宗!!!!! int n,m; int vis[1<<N]; // 预处理状态是否合法 signed main(){ while(cin>>n>>m){ if(n==0&&m==0)return 0; memset(f,0,sizeof(f)); // 多测清空 f[0][0]=1; // 初始状态 // 预处理 for(int i=0;i<(1<<m);i++){ //cnt 记录每一段的奇偶性 // has_odd 记录是否有一段是有奇数个0的 bool cnt=0,has_odd=0; // 一个合法状态中, 连续的0 必须有偶数个 for(int j=0;j<m;j++){ // (i>>j)&1 是判断状态i的第j位是否为1,为1就开始新的一段 if((i>>j)&1)has_odd|=cnt,cnt=0;// cnt 重置 else cnt^=1; // cnt 记录每一段的奇偶 } //! 这个必须是 has_odd | cht //! cnt 算的是最后一段的,没有记录在has_odd 里面 vis[i]= has_odd|cnt ? 0 :1; } 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)==0 && vis[j|k]) f[i][j]+=f[i-1][k]; } } } cout<<f[n][0]<<"\n";// 输出 } return 0; } ``` ## 后记 文章整体借鉴了小蓝书《算法竞赛进阶指南》有一部分摘抄自其中。 希望可以帮助到一些初学状压的同学。