来一个通俗易懂的状压
做这题时并不知道这是插头 D P DP DP的模板,于是自己手糊了个状压,复杂度一般,但是能过,然后交上去, r a n k 3 ? rank3? rank3?上面2个打表?看来状压常数还是不错的.
思路:
先看数据范围, n ∗ m < = 100 n*m<=100 n∗m<=100,那么 n , m n,m n,m中较小的一个肯定小于等于 10 10 10,同时有个很显然的性质,把 n , m n,m n,m互换不会对结果产生影响,所以我们可以将小的作为 m m m,于是每一行就可以进行状态压缩了
因为骨牌是 1 ∗ 2 1*2 1∗2的,所以要么横着放,要么竖着放,横着放需要满足空位必须是二的倍数,而竖着放就会对后面有影响.
于是我们可以设 D P DP DP状态 f [ i ] [ s t ] f[i][st] f[i][st]表示第 i i i行状态为 s t st st,的方案数,其中 s t st st为m个格子分别是怎么填的,1表示这个格子要从这行开始竖着放,如 f [ 2 ] [ 100010 ] f[2][100010] f[2][100010]表示第二行的第一和第五个格子都要放一个竖着的骨牌(其它先不管).
然后我们枚举前一排的状态,如果前一排的状态 s t ′ st' st′& s t ! = 0 st!=0 st!=0,就意味着上一排的竖着的骨牌占了这一排竖着的骨牌的位置了,那么就不合法.
将 s t st st| s t ′ st' st′后,我们就知道当前枚举的状态中有多少个格子被填充了,剩下的就只能横着放,我们就判断这个状态中,0是否成双出现,否则不合法,这个操作可以 O ( m ) O(m) O(m)判断,最后确定都合法后,我们就把 f [ i − 1 ] [ s t ′ ] f[i-1][st'] f[i−1][st′]的值加入 f [ i ] [ s t ] f[i][st] f[i][st]就行了.
时间复杂度及优化
按照朴素想法,每一行扫一遍,枚举当前行状态,枚举上一行状态,判断0成双出现,时间复杂度 O ( n ∗ 2 m ∗ 2 m ∗ m ) = O ( n m 2 2 m ) O(n*2^m*2^m*m)=O(nm 2^{2m}) O(n∗2m∗2m∗m)=O(nm22m)就算能过也被插头 D P DP DP吊打,相当没面子,所以我们可以想一些优化:
优化 1 : 1: 1:枚举上一行状态的优化
这是做状压时的常用技巧.很显然因为 s t st st& s t ′ = 0 st'=0 st′=0,所以 s t ′ st' st′必然是 s t st st的补集的子集,以 10010 10010 10010为例,合法的状态肯定是 01101 01101 01101的子集 01100 01100 01100, 01001 01001 01001, 01000 01000 01000, 00101 00101 00101, 01100 01100 01100等等,于是我们可以写下以下玄学的代码
//Maxn为(1<<m)-1,chk为判断0成双出现的函数
for(int j=0;j<=Maxn;++j){
f[i][j]=0;
int o=Maxn^j;
for(int k=o;k;k=(k-1)&o){
if(chk((j|k),m)){f[i][j]+=f[i-1][k];}
}
if(chk(j,m)){f[i][j]+=f[i-1][0];}
}
可以证明,上面2个
f
o
r
for
for循环的复杂度是
O
(
3
m
)
O(3^m)
O(3m)次方的,然而我不会证信息学不需要证明!
于是复杂度变成了 O ( n m 3 m ) O(nm3^m) O(nm3m)
优化2:记住算过的值
事实上,chk函数只需要判断 ( 1 < < m ) ∗ m ) (1<<m)*m) (1<<m)∗m)种状态,那么我们可以把chk过的数记录下来,再碰到就直接输出结果
这个然后复杂度就变成 O ( n 3 m ) O(n3^m) O(n3m)了
另外,听另一篇题解说有很多重复的问题,于是我们可以再记录算过的答案,进一步剪枝
于是其实和插头 D P DP DP的 O ( n m 2 m ) O(nm2^m) O(nm2m)在 m < = 10 m<=10 m<=10时其实相差无几了,实际测试时因为常数小所以跑得飞快
优化3?:记录每个与当前 s t st st or运算后合法的 s t ′ st' st′
这个留给大家试试吧,还没有付诸实践,m=10时,所有有效的组合大概是 3 m 3^m 3m的十分之一,可能会再快一点(然而已经 0 m s 0ms 0ms了,所以也懒得试了)
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
bool vis[1<<11|1][11],ok[1<<11|1][11];
//用于判断该状态中0是否成双出现
int n,m;
long long f[103][1<<11|1],ans[103][103];
//f用于DP,ans保存答案
bool chk(int a,int m){
if(vis[a][m]){return ok[a][m];}
vis[a][m]=1;
int s=0,tmp=a;//a会改变,记得先存在tmp里
for(int i=1;i<=m;++i){//不能用while因为可能有前缀0
if(a&1){if(s&1)return ok[tmp][m]=0;}
else ++s;
a>>=1;
}
if(s&1)return ok[tmp][m]=0;
ok[tmp][m]=1;
return 1;
}
int main(){
for(int i=0;i<=100;++i)for(int j=0;j<=100;++j)ans[i][j]=-1;
while(~scanf("%d%d",&n,&m)){
if(n<m)swap(n,m);//m取小的
if(!n||!m){puts("0");continue;}
if(ans[n][m]!=-1){printf("%lld\n",ans[n][m]);continue;}
int Maxn=(1<<m)-1;
f[0][0]=1LL;
for(int i=1;i<=n;++i)
for(int j=0;j<=Maxn;++j){
f[i][j]=0;//先清零
int o=Maxn^j;
for(int k=o;k;k=(k-1)&o){
if(chk((j|k),m)){f[i][j]+=f[i-1][k];}
}
if(chk(j,m)){f[i][j]+=f[i-1][0];}
}
ans[n][m]=f[n][0];
printf("%lld\n",f[n][0]);
}
return 0;
}