题目
n×m的棋盘可以摆放不同的1×2小方格的种类数。
题目选自《算法竞赛进阶指南》,用于状态压缩的理解,较难。
考点
状态压缩dp ,二进制数据预处理 ,二进制运算
分析
摆放方块的时候,先放横着的,再放竖着的。
总方案数等于只放横着的小方块的合法方案数。
状态压缩的dp:用一个N位的二进制数,每一位表示一个物品,0/1表示不同的状态。‘
dp状态表示
- 状态数组f[i][j]:表示已经将前 i -1 列摆好,且从第i−1列,伸出到第 i列的状态是 j的所有方案。其中j是一个二进制数,用来表示哪一行的小方块是横着放的,其位数和棋盘的行数一致。
- 状态是如何转移
需要看 第i-2 列是怎么转移到到第 i-1列的。假设此时对应的状态是k(第i-2列到第i-1列伸出来的二进制数,比如00100),k也是一个二进制数,1表示哪几行小方块是横着伸出来的,0表示哪几行不是横着伸出来的。对应的方案数是 f[i−1,k]。
3.k需要满足说明条件 ((j & k) == 0 && st[j | k]) 关于这个条件的意义在代码注解里有解释
代码
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 12,M = 1 << N;
long long f[N][M];
bool st[M];
//记录合法状态的数组
vector<vector<int>> state(M);
int n,m;
int main()
{
//n为宽 m为长
while(cin >> n >> m,n || m)
{
//预处理1 ——排除每列出现奇数个连续0的情况
//因为列状态如果含有奇数个连续的0 说明竖着放长方形时无法填满 不合法
for(int i = 0;i < 1 << n;i++)
{
int cnt = 0;//记录连续0的个数
bool isValid = true;//某种状态没有奇数个连续的0则标记为true
for(int j = 0;j < n;j++)//从上到小遍历当前列
{
//i是一个十进制的数字
//i>>j 是循环从i中提取二进制的j位
//&1为判断该位是否为1,如果为1进入if
if((i >> j) & 1)
{
//cnt & 1为真则该状态包含奇数个0
//因为cnt如果是个奇数那么cnt二进制最后一位一定是1
//1 & 1 则为真 满足if条件
if(cnt & 1)
{
isValid = false;
break;
}
}
else cnt ++; //j位为0 cnt++
}
//处理可能残留的cnt
if(cnt & 1) isValid = false;
//状态i是否有奇数个连续的0的情况存入st数组
st[i] = isValid;
}
//预处理2
//判断第i-2列伸出来的和第i-1列伸出去的是否冲突
for(int j = 0;j < 1 << n;j++) //j代表第i列所有状态
{
state[j].clear();//清理数组,因为这些逻辑是在while循环内不是处理一组数据
for(int k = 0;k < 1 << n;k++)//k代表第i-1列所有状态
{
//两列的状态 存在 不能重叠 (j & k) == 0
//j | k用于合并在i-1列有多少个1
//st[j | k]用于排除不合法的状态
if((j & k) == 0 && st[j | k]) state[j].push_back(k);
//二维数组state[j]表示在数组的第j行 相当于j作为key
//j表示第i列“真正”可行的状态,
//如果第i-1列的状态k和j不冲突则压入state数组中的第j行。 k相当于value
//“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
}
}
//dp部分
memset(f,0,sizeof f);//全部初始化为0,因为是连续读入,这里是一个清空操作。类似上面的state[j].clear()
f[0][0] = 1;
//虚构的状态数组初始化
//f[0][0]第一个0表示第0列,
//第二个0表示第0列伸出去的状态为0,
//那么第0列的方案只能是竖摆这一种,至于不需要考虑竖摆的奇偶数合法性
//因为第0列本身就是为了推导第1列而虚构的。
//如果假设f[0][0]=0,那么后面所有的递推就无从谈起。
//dp
for(int i = 1;i <= m;i++)//遍历每一列
{
for(int j = 0;j < 1 << n;j++)//遍历每列的状态
{
for(auto k : state[j]) //遍历第i-1列的合法的状态k,并进行转移
{
f[i][j] += f[i-1][k];//当前列的方案数就等于之前的第i-1列所有状态k的累加。
}
}
}
//f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
cout << f[m][0] << endl;
}
return 0;
}