hdu 4064 Carcassonne——11年福州网络赛状压dp

5 篇文章 0 订阅
1 篇文章 0 订阅

呃,显然这题是典型的插头dp题?


不过弱菜本来就不是搞dp的料,所以插头dp没怎么了解过。看了http://liulixiang.info/ac/?p=88,这位大牛的报告,觉得这题用普通的状压思想理解可能更方便点。

 

本博客的定位也就是为了让更多像我一样还不是大牛的人能从弱菜的角度去看题解题。所以我也就只能写到这个程度了。希望能多写点注释,让对状压dp不太熟悉的ACMER能看懂,学会怎么做这题。

其实我的代码基本都是参考了上面那篇报告的,因为我太弱了,看完后整个思想就跟着走了。

这题状态其实挺明显,就是dp[row][status],status就是对于前row行,最后一行的下面的字母构成的状态的方法数。然后状态转移很简单。就是dp[row][status]=sum(dp[row-1][up_status])。就是能构成当前状态的当前行的上面字母构成的状态的方法和。因为当前行的上面字母应该和上一行的下面字母是一样的。所以这个状态可以O(1)的对应的。

具体实现的时候就是枚举每一行的所有可能的状态。即每个块枚举四个旋转,所以就可能有4^m种(实际情况没有那么多,因为只有3个字母)。这里如果不考虑重复状态的话,每次枚举都是4^m次的,4^m*n的复杂度还是相当高的,在极限数据下是过不了的。时限只有1秒。所以这里必须优化。

也就是上面那篇报告里面的方法。具体就是在枚举某个块四个旋转时,记录下这四个旋转中是否会产生相同的状态(即某次旋转和之前的是一样的,比如四个字母相同的情况),这时候枚举的次数就会减少。比如四个状态都一样的话,只需要递归一次即可。根据乘法原理,把相同状态的次数乘起来即可。就是当这行都匹配合法后,这种状态由于重复出现的次数统计起来。拿这个次数sum*dp[row-1][pre_status]就是这种状态可以被拓展的次数。呃,有点说不清,具体还是看代码吧。总之这个优化使用后,效率会高很多。比如只要有一个块是四个字母一样,那某行就变成4^(m-1)了。再考虑只有三个字母的情况下,很多状态都会被合并处理。

代码附了一些注释,希望不熟悉状压dp的同志能看懂:

#include <algorithm>
#include <cstdlib>
#include <cctype>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <vector>
#include <string>
#include <iostream>
#include <ctime>
using namespace std;
typedef __int64 ll;
#define MOD  1000000007
#define MAX 531443
int dp[2][MAX];//3^12
char st[13][13][6];
int n,m,Max,cur,row;
void init()
{
    int i,j;
    scanf("%d%d",&n,&m);
    for (i=0;i<n;i++)
        for (j=0;j<m;j++)
            scanf("%s",st[i][j]);
    Max=1;
    for (i=0;i<m;i++) Max*=3;
    for (i=0;i<Max;i++) dp[0][i]=1;//初始化,因为是累乘的。所以一开始要初始化为1
}
int change(char x)//转成3进制,便于表示状态
{
    if (x=='R') return 1;
    if (x=='F') return 2;
    return 0;
}
void dfs(int col,ll sum,char prechar,int pre,int now)
{
    if (col==m)//该行合法的一种状态,累加答案
    {
        sum = (sum * dp[cur^1][pre]) % MOD;//该状态能组成的方法数*上层状态的方法数。乘法原理
        dp[cur][now] = (dp[cur][now] + sum) % MOD;
        return ;
    }
    int cnt=0,num[5];
    char top[5],down[5],right[5];
    for (int i=0;i<4;i++)//枚举四种旋转
    {
        if (col!=0 && st[row][col][i]!=prechar) continue;//相邻不同不能连接
        //分别记录上,下,右的字母类型
        char tt=st[row][col][(i+1)%4],
             td=st[row][col][(i+3)%4],
             tr=st[row][col][(i+2)%4];
        bool ck=1;//判断是否重复
        for (int j=0;j<cnt;j++)//判断是否有旋转后相同的状态,累计起来。这是最重要的剪枝
        {
            if (top[j]==tt && down[j]==td && right[j]==tr)
            {
                ck=0;
                num[j]++;//该状态在该块出现的次数
                break;
            }
        }
        if (ck)//没相同,记录新的状态
        {
            top[cnt]=tt;
            down[cnt]=td;
            right[cnt]=tr;
            num[cnt++]=1;
        }
    }
    for (int i=0;i<cnt;i++)
    {
        int Pre=pre*3+change(top[i]);//上面的字母的新状态
        int Now=now*3+change(down[i]);//下面字母的新状态
        dfs(col+1,(sum*num[i] % MOD),right[i],Pre,Now);
    }
}
void solve()
{
    int i,j;
    cur=0;
    for (row=0;row<n;row++)//枚举行
    {
        cur^=1;
        for (j=0;j<Max;j++)
            dp[cur][j]=0;
//        memset(dp[cur],0LL,sizeof(dp[cur]));
        dfs(0,1LL,' ',0,0);//col val prechar
    }
}
int main()
{
    int T,i;
    scanf("%d",&T);
    for (int ca=1;ca<=T;ca++)
    {
        init();
        solve();
        int ans=0;
        for (i=0;i<Max;i++)
            ans = ( ans + dp[cur][i] ) % MOD;
        printf("Case %d: %d\n",ca,ans%MOD);
    }
    return 0;
}



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值