引言
- 首先本题与ACwing的另一题《蒙德里安的梦想》一模一样,只是那题是多样例点
然后本题是本人入坑状态压缩dp的第一题,有纪念意义,故作此记录
题目
思路
状态压缩dp新手引入
时间复杂度
- 状态压缩dp实际上也是一种程度上的暴力做法,通过枚举所有情况,得到答案。但不同于普通dfs,dfs的复杂度一般是N!,而状态dp一般是N
*
2^N 或者N*
2^N*
2^N。 当N=20时,前者接近10^18 , 而后者仅有10^12
个人入坑最开始难以理解的点
- 然后是状态压缩dp本身了,这里最重要的是不要带入传统dp的观点认为他是由一个或几个状态得到,他是由上一次的所有结果得到,一般来说是2^N 个结果得到。 也就是状态压缩
d p [ i ] [ s t a t e ] = ∑ k = 0 k = 2 N − 1 d p [ i − 1 ] [ k ] {\mathop{\rm dp}\nolimits} [i][state] = \sum\nolimits_{k = 0}^{k = {2^N} - 1} {dp[i - 1][k]} dp[i][state]=∑k=0k=2N−1dp[i−1][k]
这里state是当前的状态,是一个二进制数(存储时是十进制存储),k是上一时刻的状态,即当前取i时全局状态为state(例如dp[2][1011]在求最短路时表示,当前点在2点同时已经走过0,1,3三个点),由前一个i-1取所有状态到当前状态得到。
题目本身
// 首先横着放好所有方块后,那么竖着的方案是唯一的,所以只用考虑横着的即可
// 设dp[i][j]为前i-1列排列好,从第i-1列伸到第i列的状态为j的总方案数,其中j为二进制数,长度与行数相同
// 则dp[i][j]=求和 k从00000到11111的所有状态dp[i-1][k],即当前状态为j由上列的所有合法情况到j的总和
合法判断
// 同时求和需要满足合法条件,即k&j==0,状态k和状态j之间不能有位相同,不然即从i-2开始一直伸到i,超出了方块的长度2
// 且要满足st[k|j]=true,st[k|j]是i-2列伸出到i-1列的k与主动从i-1列伸到i列的j的并集
// 即是i-1列被两种情况包围后的实际被横着的格子,需要判断是否合法
// 不对st[j]和st[k]进行合法判断,是因为他们都只是半吊子,还有无限可能性
// 但前后夹击后唯一确定了中间那列的排列方法,需要判断夹击后的列是否合法
初始化
// 初始化dp[1][x]=1,即从0列横着放到伸出到第一列的状态x,就是一种情况
// 总共n行m列,序号从0开始
// 最终dp[m][0]即为所求,表示的是前m-1列全部填好,且第m列为全0,即m-1列没有伸出的。
// 即最终答案m比下标m-1大,因为m才代表前m-1列都排列好了
总结
// 实际做法:1.先可以进行一列内状态的合法性判定,即一个状态中连续为空的个数是否为奇数,如果为奇数,则无法竖着填满,不合法
// 2.初始化dp[1][x]=1
// 3.计算dp数组得到答案
代码
//
// Created by starplatinum on 2024/5/16.
//
#include<iostream>
using namespace std;
// 最大的行数和列数
const int N=12;
// 状态数是2的行数次方
const int state=1<<N;
int main(){
int dp[N][state]={0};
// 存储列内状态是否合法
bool st[state];
int n=8;
int m=8;
// 判断状态是否合法
for(int i=0;i<(1<<n);i++){
st[i]= true;
int sum=0;
// 2的n次方从右往左依次是0到第n-1位,判断连续的0是否为奇数个
for(int j=0;j<n;j++){
// 将i的二进制数右移j位,此时个位的数即是i原来的第j位
// 判断第j位是否为1,为1则可以判断之前记录的空的数量
if((i>>j)&1){
if(sum%2){
st[i]= false;
break;
}
} else{
sum++;
}
}
// 都结束还没遇到1,说明还没有结算空格的个数
if(sum%2){
st[i]= false;
}
}
// 初始化dp[1][x]=1,合法的才为1
for(int i=0;i<(1<<n);i++){
if(st[i]){
dp[1][i]=1;
}
}
// 计算dp数组
for(int i=2;i<=m;i++){
for(int j=0;j<(1<<n);j++){
for(int k=0;k<(1<<n);k++){
// st[k|j]是i-2列伸出到i-1列的k与主动从i-1列伸到i列的j的并集
// 即是i-1列被两种情况包围后的实际被横着的格子
// 不需要判断k和j,因为他们都只是半吊子,有无限可能
// 但当他们结合起来,就能唯一确定中间列的情况,需要对这个情况进行合法性判定
if(((k&j)==0)&&st[k|j]){
dp[i][j]+=dp[i-1][k];
}
}
}
}
cout<<dp[m][0]<<endl;
return 0;
}
后记
我在做时参考了这篇博客这篇博客,写的很好
然后还有很多类似的题,例如我就是从蓝桥杯的另一题《补给》跳到这里。也许后续会更新补给的思路。