UVA11270 【Tiling Dominoes】插头DP状压搞定

题面

来一个通俗易懂的状压

做这题时并不知道这是插头 D P DP DP的模板,于是自己手糊了个状压,复杂度一般,但是能过,然后交上去, r a n k 3 ? rank3? rank3?上面2个打表?看来状压常数还是不错的.

思路:

先看数据范围, n ∗ m &lt; = 100 n*m&lt;=100 nm<=100,那么 n , m n,m n,m中较小的一个肯定小于等于 10 10 10,同时有个很显然的性质,把 n , m n,m n,m互换不会对结果产生影响,所以我们可以将小的作为 m m m,于是每一行就可以进行状态压缩了

因为骨牌是 1 ∗ 2 1*2 12的,所以要么横着放,要么竖着放,横着放需要满足空位必须是二的倍数,而竖着放就会对后面有影响.

于是我们可以设 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&#x27; st& s t ! = 0 st!=0 st!=0,就意味着上一排的竖着的骨牌占了这一排竖着的骨牌的位置了,那么就不合法.

s t st st| s t ′ st&#x27; st后,我们就知道当前枚举的状态中有多少个格子被填充了,剩下的就只能横着放,我们就判断这个状态中,0是否成双出现,否则不合法,这个操作可以 O ( m ) O(m) O(m)判断,最后确定都合法后,我们就把 f [ i − 1 ] [ s t ′ ] f[i-1][st&#x27;] f[i1][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(n2m2mm)=O(nm22m)就算能过也被插头 D P DP DP吊打,相当没面子,所以我们可以想一些优化:

优化 1 : 1: 1:枚举上一行状态的优化

这是做状压时的常用技巧.很显然因为 s t st st& s t ′ = 0 st&#x27;=0 st=0,所以 s t ′ st&#x27; 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 &lt; &lt; m ) ∗ m ) (1&lt;&lt;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 &lt; = 10 m&lt;=10 m<=10时其实相差无几了,实际测试时因为常数小所以跑得飞快

优化3?:记录每个与当前 s t st st or运算后合法的 s t ′ st&#x27; 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;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值