状态压缩DP,是今天所要讲到的内容。
其实状态压缩这个概念我们并不陌生,我们之前在做八数码问题的时候就是把那张图给压缩成了一串数字来表示,这里其实也是利用到了状态压缩,让图的内容可以很简单的用数来表示。
题解:我们来分析一下这道题该怎么做,以及我们该怎么进行状态表示以及状态转移;
首先我们可以知道这是个N X M的棋盘,所以我们可以把这个棋盘先画出来,然后看一下状态该怎么表示。
如图所示,我们可以看到在这个6X7的棋盘中,我们画了一些横着的红色边框的1X2的方块,那么剩下的我们可以用2X1的竖着的小方格全部补齐,所以我们发现分割该棋盘的方案数等于在这个棋盘中布局横着的小方块的方案数,但是我们要怎么去找到一个方式表示横着的方块呢 ?
所以,我们选择枚举横着的小方块,我们可以假设(i ,j)这个点被一个横着的小方块占据,所以可以发现要么被占据要么不被占据,只有这两种情况,所以我们自然而然地想到了可以用二进制来表示这个状态,被占据就是1,不被占据就是0,所以我们把每一列都可以找出一个状态,只要这个状态是合法的,我们就可以让方案数加一。
接下来考虑什么样是合法的状态的问题,首先,我们枚举的是横着的小方块,说明剩下的格子一定能被竖着的方块填充,所以不会出现奇数个连续的0的情况,而且因为这是个横着的小方块,就假设我们枚举的点是后一块,那么前面还有一块,那么此时就要注意了,如果(i , j)这个位置是1,(i - 1 , j)这个位置就只能是0,因为这个点不能同时是后小方块的前一个点也是前一个小方块的后一个点。
接下来进行状态表示:f[i][j] 表示的是在前i-1列都能满足的条件下,第i列的状态j,也就是第i列哪些点是被横着的小方块的后一个点覆盖的情况下,方案数是多少,所以最后的答案是f[m][0] , 因为我们相当于是从棋盘的第二列开始枚举(因为第一列的点不能成为横着的小方块的后一块),所以在前m列都满足的情况下,没有一个方块超出棋盘的情况。
状态转移方程:对于第i列取状态j时,i-1列的状态只能取k(k∈state[i])的方案数之和)
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int n , m;
const int N = 1 << 12;
bool st[N];
vector<int> state[N];
LL f[12][N];
int main()
{
while(cin >> n >> m , n || m)
{
//这一段是通过遍历状态i的可能性,预处理把可能的i都找出来放到st数组判断
for(int i = 0; i < 1<<n ;i++)
{
bool is_valid = true;
int cnt = 0;
for(int j = 0; j<n; j++)
{
if(i >> j & 1){
if(cnt & 1){
is_valid = false;
break;
}
cnt = 0;
}
else cnt ++;
}
if(cnt & 1) is_valid = false;
st[i] = is_valid;
}
//这是第二次预处理,直接找出状态i能够配对的状态j,存入不定长数组state中
for(int i = 0; i < 1 << n; i++)
{
state[i].clear();
for(int j = 0;j < 1 << n; j++)
{
if ((i & j) == 0 && st[j | i]) state[i].push_back(j); //为什么要 j | i,是为了让后一列状态为j,前一列状态为i时都能合法,因为我们第一步预处理的时候只考虑这一列为当前状态的时候能否合法,也就是说只考虑横着的小方块后一半带来的影响,而没考虑前一半带来的影响。
}
}
//这里是得出答案
memset(f,0,sizeof f);
f[0][0] = 1;
for(int i = 1;i<=m;i++)
{
for(int j = 0; j < 1 << n; j++)
{
for(auto t : state[j])
f[i][j] += f[i-1][t];
}
}
cout << f[m][0] << '\n';
}
return 0;
}
·····················不定期更新状态压缩DP的例题··········································