题目描述
对于一个行数为n,列数为m的方格网盘,用若干个2行1列的长方形去填充它,问一共有多少种方案?
输入描述
第一行为 n n n ( 1 ≤ n 1\leq n 1≤n ≤ 11 \leq 11 ≤11), m m m ( 1 ≤ m 1\leq m 1≤m ≤ 11 \leq 11 ≤11)
输出描述
输出方案数
样例
input
2 3output
3分析
本题采用的方法为状压dp,用0和1来表示网格中小矩形的放置情况。
小矩形有两种放置情况:横着放和竖着放。如果是是竖放,那么把上一格记为1,下一格记为0;如果是横放,那么把两个方格都记为0,如下所示:
于是,对于某一种放置方法,可以用
n
∗
m
n * m
n∗m 大小的唯一的 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 i−1 行的所有状态,在这两行所有的组合状态中,筛选出所有可行的组合。
对于验证后可行的组合为 s t a t e i state_i statei 与 s t a t e i − 1 state_{i-1} statei−1 ,它们都是一个二进制串。例如, s t a t e i state_i statei = 111 与 s t a t e i − 1 state_{i-1} statei−1 = 000 是兼容的,也可以说 s t a t e i state_{i} statei = 7 与 s t a t e i − 1 state_{i-1} statei−1 = 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[i−1][0] ,它表示,当前行状态7的可放置情况等于当前放置数累加上上一行状态0的可放置情况总数。
由于动态规划是从前向后顺序更新的,所以访问第 i i i 行时,上一行所有状态的可放置数已经是确定的了。 只需要考虑当前行可行的状态,然后找到与该状态兼容的所有状态,将所有兼容的状态累加到当前状态下,便可得到当前 d p [ i ] [ j ] dp[i][j] dp[i][j] 的大小了。
题解
①判断第 i i i 行与第 i − 1 i - 1 i−1 行是否兼容。
根据前文,第 i i i 行与第 i − 1 i - 1 i−1 行如果兼容,需要满足两个情况,首先,第 i i i 行与第 i − 1 i - 1 i−1 行上下不能有两个连续的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 i−1 行做或运算后,没有奇数个连续0的兼容情况放到数组中。例如,对于4列的网格,其所有兼容情况为:
只要保证第
i
i
i 行与第
i
−
1
i - 1
i−1 行没有上下连续的两个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,推荐试一试!!