在acwing上学习算法的一点思考与总结
在状态压缩中,我们使用整数的二进制表示来表示一个集合的状态,以减少内存消耗和提高计算效率。下面解析一道经典的状态压缩dp的题。
思路
如何解:摆放方块的时候,先放横着的,再放竖着的。总方案数等于只放横着的小方块的所有合法方案数。合法的意思是在一列中必须要有能放得下竖直长方形的位置,其次在放横着的长方形时,i与i-1列不能有所交叠。
核心:第i列的摆放状态只与第i-1列的摆放状态相联系,这意味着我们在考虑第计算第i列的合法方案数时只需要考虑第i-1列的状态就行了。
二进制表示状态:为了表示一列里面的摆放方式,我们可以用二进制来枚举他所有可能的摆放方式。(这里的摆放方式只有横放)。比如1100,表示第i列的前两行都有从i-1横着放过来的长方形,后两行则为空。那么一列中会有2^n种状态。
状态转移方程:所以我们的状态表达式f[ i ][ j ]的含义就是第 i 列的 j 状态(j是十进制数)。状态转移方程就是将符合合法状态的方案数累加: f [ i ][ j ] += f [ i - 1 ][ k ] (将第i列j状态的合法方案数与前一列k状态的合法方案数累加)
判断合法:1.用位运算得到二进制的第i位数,记录连续个零的个数
2.枚举两列不同状态组合下是否满足两个条件
第一个条件:第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行)
第二个条件: 第i-1列此时的状态是否合法
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 13, M = 1 << N;
long long f[N][M]; //第一维表示列,第二维表示所有可能的状态
bool st[M]; //该状态是否存在连续奇数个0
int n, m;
vector<int > state[M]; //存放第i列与第i-1列合法状态的集合,用一个二维数组储存。
int main()
{
while(cin>>n>>m, n || m) //当n,m都为零时退出循环
{
for(int i = 0; i < (1<<n); i ++ )
{
int cnt = 0; //记录连续个零的数量
bool valid = true;
for(int j =0; j < n; j++)
if((i >> j) & 1) // i >> j 表示i的二进制数的第j位,如果第j位是1,需执行下面的判断
{
if(cnt & 1) //如果前面零的数量为奇数,则该列状态不合法
{
valid = false; //标记为false
break;
}
cnt = 0; //相反如果数量是偶数,则将cnt清零
}
else cnt ++;
if(cnt & 1) valid = false; //将剩下的零的数量进行判断
st[i] = valid; //将该状态的合法情况录入st数组
}
//寻找相邻两列合法时的i和i-1状态的组合
for(int j = 0; j < (1<<n); j ++) //枚举第i列状态
{
state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态
for(int k = 0; k< (1<<n); k ++) //枚举第i-1列状态
if(( j & k) == 0 && st[j | k] )
//第一个条件:第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行)
//第二个条件: 第i-1列的此时状态是否合法
state[j].push_back(k); //将合法情况加入到二维数组中
}
memset(f, 0, sizeof f);
f[0][0] = 1; //因为没有第-1列,所以也就不可能横着到第0列,那么就是只有竖着放这1种状态
for(int i = 1; i<= m; i ++) //遍历每一列
for(int j = 0; j < (1 << n); j++) //遍历第i列的所有状态
for(auto k:state[j]) //遍历第i-1列的所有状态,如果有符合合法状态的k,就执行状态转移方程
f[i][j] += f[i-1][k]; //将第i列j状态的方案数与前一列k状态的方案数累加
cout<<f[m][0]<<endl; //意味最后一列不再有横着的长方形伸出去,即所有方案数都已处理完
}
}