hdu 1693 Eat the Trees

Eat the Trees

Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 1944    Accepted Submission(s): 939


Problem Description
Most of us know that in the game called DotA(Defense of the Ancient), Pudge is a strong hero in the first period of the game. When the game goes to end however, Pudge is not a strong hero any more.
So Pudge’s teammates give him a new assignment—Eat the Trees!

The trees are in a rectangle N * M cells in size and each of the cells either has exactly one tree or has nothing at all. And what Pudge needs to do is to eat all trees that are in the cells.
There are several rules Pudge must follow:
I. Pudge must eat the trees by choosing a circuit and he then will eat all trees that are in the chosen circuit.
II. The cell that does not contain a tree is unreachable, e.g. each of the cells that is through the circuit which Pudge chooses must contain a tree and when the circuit is chosen, the trees which are in the cells on the circuit will disappear.
III. Pudge may choose one or more circuits to eat the trees.

Now Pudge has a question, how many ways are there to eat the trees?
At the picture below three samples are given for N = 6 and M = 3(gray square means no trees in the cell, and the bold black line means the chosen circuit(s))


 

Input
The input consists of several test cases. The first line of the input is the number of the cases. There are no more than 10 cases.
For each case, the first line contains the integer numbers N and M, 1<=N, M<=11. Each of the next N lines contains M numbers (either 0 or 1) separated by a space. Number 0 means a cell which has no trees and number 1 means a cell that has exactly one tree.
 

Output
For each case, you should print the desired number of ways in one line. It is guaranteed, that it does not exceed 2 63 – 1. Use the format in the sample.
 

Sample Input
  
  
2 6 3 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 2 4 1 1 1 1 1 1 1 1
 

Sample Output
  
  
Case 1: There are 3 ways to eat the trees. Case 2: There are 2 ways to eat the trees.
 

Source
 

Recommend
wangye
         插头dp第一题。研究了一天的插头dp。看了各种论文PPT。不过都不是那么浅显易懂的。要边思考边自己画画图才能理解其中的思想。插头路还漫长。同志仍需努力!

状态转移以轮郭线上插头为转移。关于轮廓线及插头相关内容见《基于连通性状态压缩的动态规划问题》
最好看论文。PPT虽然华丽但没有论文里讲的详细。由于是回路每个格子都要被走一次。所以每个格子都有两个插头就如欧拉回路每个点的度数为偶数。一个进一个出。n*m的格子轮廓线上有m个下插头一个右插头。如果当前状态轮廓线上已经有两个插头就无需再添加插头了。如果有一个下插头或右插头则添加一个下插头或右插头就行。若无插头则必须添加一个下插头和右插头。每次通过上一状态的j和j+1判断maze[i][j]处轮廓线的插头数进行状态转移。见上图。代码如下:
#include <iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define MAX(a,b) ((a)>(b)?(a):(b))
#define MIN(a,b) ((a)<(b)?(a):(b))
#define positive(a) ((a)>0?(a):-(a))
using namespace std;
__int64 dp[12][12][1<<12];//dp[i][j][s]代表第i行第j列轮廓线上插头状态为s的方法数
int maze[12][12];
int n,m;

void solve()
{
    int i,j,sta,left,down,right,up;
    memset(dp,0,sizeof dp);
    dp[0][m][0]=1;
    for(i=1; i<=n; i++)//上一层转移到下一层的特殊处理
    {
        for(sta=0; sta<(1<<m); sta++)//因为上一层的右插头已经没用所以对下一层有影响的为sta<<1。
            dp[i][0][sta<<1]=dp[i-1][m][sta];
        for(j=1; j<=m; j++)
        {
            right=up=1<<j;//状态转移为maze[i][j-1]的右插头
            left=down=1<<(j-1);//位运算移j-1位相当于取出第j位
            for(sta=0; sta<(1<<(m+1)); sta++)//注意有m+1个状态多了个左插头
            {
                if(maze[i][j])//若不是树
                {
                    if((sta&right)&&(sta&down))//如果已经有两个插头了
                        dp[i][j][sta]=dp[i][j-1][sta-up-left];//状态转移为sta-up-left--->sta
                    else if(!(sta&right)&&!(sta&down))//如果一个插头也没有
                        dp[i][j][sta]=dp[i][j-1][sta+up+left];
                    else//只有一个插头的有两种情况
                        dp[i][j][sta]=dp[i][j-1][sta]+dp[i][j-1][sta^up^left];//为运算用的很好
                }
                else//若是树
                {
                    if(!(sta&right)&&!(sta&down))
                        dp[i][j][sta]=dp[i][j-1][sta];
                    else
                        dp[i][j][sta]=0;
                }
            }
        }
    }
    printf("There are %I64d ways to eat the trees.\n",dp[n][m][0]);
}
int main()
{
    int t,cas=1,i,j;

    scanf("%d",&t);
    //freopen("data.txt","w",stdout);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        for(i=1; i<=n; i++)
            for(j=1; j<=m; j++)
                scanf("%d",&maze[i][j]);
        printf("Case %d: ",cas++);
        solve();
    }
    return 0;
}

    看了大牛的博客后。发现这类问题可以模板化。于是借鉴了下大牛的模板修改了一些地方。以后插头代码基本就这格式了。

#include<stdio.h>
#include<string.h>
#include<iostream>
#include<algorithm>
using namespace std;

const int HASH=10007;//哈希表的大小
const int STATE=1000010;//状态数
const int MAXD=15;
int N,M;
int code[MAXD],maze[MAXD][MAXD];//编码。和存地图

struct HASHMAP//哈希表结构
{
    int head[HASH],next[STATE],state[STATE],sz;//哈希表头指针模数相同的状态用链表连接。方便状态查找和判重
    long long f[STATE];//f记录对应状态的方法数。next指向模数相同的下一个状态。state记录状态。sz记录状态总数
    void init()//哈希表初始化函数
    {
        sz=0;
        memset(head,-1,sizeof(head));
    }
    void push(int st,long long ans)//压入状态和方法数
    {
        int i,h=st%HASH;
        for(i=head[h];i!=-1;i=next[i])
          if(st==state[i])//若状态已经存在。方法数增加就行
          {
              f[i]+=ans;
              return;
          }
        f[sz]=ans;//存入新的可行状态
        state[sz]=st;
        next[sz]=head[h];
        head[h]=sz++;
    }
}hm[2];

void decode(int *code,int m,int st)//编码从低位到高位0到m编码。对应从左到右的插头
{
    int i;
    for(i=0;i<=m;i++)
    {
        code[i]=st&1;
        st>>=1;
    }
}
int encode(int *code,int m)//解码到st中
{
    int i,st=0;
    for( i=m;i>=0;i--)
    {
        st<<=1;
        st|=code[i];
    }
    return st;
}
void init()//读数据。初始化
{
    int i,j;
    scanf("%d%d",&N,&M);
    for(i=1;i<=N;i++)
      for( j=1;j<=M;j++)
        scanf("%d",&maze[i][j]);
    for(int i=1;i<=N;i++)maze[i][M+1]=0;//边界补0
    for(int i=1;i<=M;i++)maze[N+1][i]=0;
}

void shift(int *code,int m)//换行的时候移位
{
    int i;
    for(i=m;i>0;i--)
      code[i]=code[i-1];
    code[0]=0;
}

void dpblank(int i,int j,int cur)//处理可到格的情况
{
    int k,left,up;
    for(k=0;k<hm[cur].sz;k++)//遍历j格出轮廓线的状态进行状态转移
    {
        decode(code,M,hm[cur].state[k]);//对状态进行编码
        left=code[j-1];//获取左插头状态
        up=code[j];//获取上插头状态
        if(left&&up)//11  -> 00
        {
            code[j-1]=code[j]=0;//只能为0一个格子只能两个插头
            if(j==M)shift(code,M);//到了边界后换行
            hm[cur^1].push(encode(code,M),hm[cur].f[k]);//压入新状态
        }
        else if(left||up)//01 或 10
        {
            if(maze[i][j+1])//j==m时maze[i][j]为0也不用shift
            {
                code[j-1]=0;
                code[j]=1;
                hm[cur^1].push(encode(code,M),hm[cur].f[k]);
            }
            if(maze[i+1][j])
            {
                code[j-1]=1;
                code[j]=0;
                if(j==M)shift(code,M);//这个不要忘了!
                hm[cur^1].push(encode(code,M),hm[cur].f[k]);
            }
        }
        else
        {
            if(maze[i][j+1]&&maze[i+1][j])//若j==m不会合法所以不用shift
            {
                code[j]=code[j-1]=1;
                hm[cur^1].push(encode(code,M),hm[cur].f[k]);
            }
        }
    }
}

void dpblock(int i,int j,int cur)//处理不能到格的情况
{
    int k;
    for(k=0;k<hm[cur].sz;k++)//存入状态均合法不用判断
    {
        decode(code,M,hm[cur].state[k]);//先编码
        code[j-1]=code[j]=0;//肯定不能用插头
        if(j==M)shift(code,M);//转移到下一行
        hm[cur^1].push(encode(code,M),hm[cur].f[k]);
    }
}

void solve()
{
    int i,j,cur=0;
    long long ans=0;
    hm[cur].init();//cur用于滚动数组。节约空间。cur存当前格上插头和左插头情况。cur^1用于记录转移出的新状态即下一格
    hm[cur].push(0,1);
    for(i=1;i<=N;i++)
      for(j=1;j<=M;j++)
      {
          hm[cur^1].init();//初始化
          if(maze[i][j])dpblank(i,j,cur);
          else dpblock(i,j,cur);
          cur^=1;
      }
    for(i=0;i<hm[cur].sz;i++)
      ans+=hm[cur].f[i];
    printf("There are %I64d ways to eat the trees.\n",ans);
}
int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
    int T;
    int iCase=0;
    scanf("%d",&T);
    while(T--)
    {
        iCase++;
        printf("Case %d: ",iCase);
        init();
        solve();
    }
    return 0;
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值