当前位置: 代码迷 >> 综合 >> UVA11270 【Tiling Dominoes】插头DP状压搞定
  详细解决方案

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

热度:62   发布时间:2023-12-14 10:09:48.0

题面

来一个通俗易懂的状压

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

思路:

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

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

于是我们可以设DPDPDP状态f[i][st]f[i][st]f[i][st]表示第iii行状态为ststst,的方案数,其中ststst为m个格子分别是怎么填的,1表示这个格子要从这行开始竖着放,如f[2][100010]f[2][100010]f[2][100010]表示第二行的第一和第五个格子都要放一个竖着的骨牌(其它先不管).

然后我们枚举前一排的状态,如果前一排的状态st′st&#x27;st&st!=0st!=0st!=0,就意味着上一排的竖着的骨牌占了这一排竖着的骨牌的位置了,那么就不合法.

ststst|st′st&#x27;st后,我们就知道当前枚举的状态中有多少个格子被填充了,剩下的就只能横着放,我们就判断这个状态中,0是否成双出现,否则不合法,这个操作可以O(m)O(m)O(m)判断,最后确定都合法后,我们就把f[i?1][st′]f[i-1][st&#x27;]f[i?1][st]的值加入f[i][st]f[i][st]f[i][st]就行了.

时间复杂度及优化

按照朴素想法,每一行扫一遍,枚举当前行状态,枚举上一行状态,判断0成双出现,时间复杂度O(n?2m?2m?m)=O(nm22m)O(n*2^m*2^m*m)=O(nm 2^{2m})O(n?2m?2m?m)=O(nm22m)就算能过也被插头DPDPDP吊打,相当没面子,所以我们可以想一些优化:

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

这是做状压时的常用技巧.很显然因为ststst&st′=0st&#x27;=0st=0,所以st′st&#x27;st必然是ststst的补集的子集,以100101001010010为例,合法的状态肯定是011010110101101的子集011000110001100,010010100101001,010000100001000,001010010100101,011000110001100等等,于是我们可以写下以下玄学的代码

//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个forforfor循环的复杂度是O(3m)O(3^m)O(3m)次方的,然而我不会证信息学不需要证明!

于是复杂度变成了O(nm3m)O(nm3^m)O(nm3m)

优化2:记住算过的值

事实上,chk函数只需要判断(1&lt;&lt;m)?m)(1&lt;&lt;m)*m)(1<<m)?m)种状态,那么我们可以把chk过的数记录下来,再碰到就直接输出结果

这个然后复杂度就变成O(n3m)O(n3^m)O(n3m)

另外,听另一篇题解说有很多重复的问题,于是我们可以再记录算过的答案,进一步剪枝

于是其实和插头DPDPDPO(nm2m)O(nm2^m)O(nm2m)m&lt;=10m&lt;=10m<=10时其实相差无几了,实际测试时因为常数小所以跑得飞快

优化3?:记录每个与当前ststst or运算后合法的st′st&#x27;st

这个留给大家试试吧,还没有付诸实践,m=10时,所有有效的组合大概是3m3^m3m的十分之一,可能会再快一点(然而已经0ms0ms0ms了,所以也懒得试了)

代码:

#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因为可能有前缀0if(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;
}