hdu 5731 Solid Dominoes Tilings 插头dp+容斥原理


Solid Dominoes Tilings

Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)
Total Submission(s): 131    Accepted Submission(s): 75


Problem Description
Dominoes are rectangular tiles with nice 2 × 1 and 1 × 2 sizes.

The tiling is called solid if it is not possible to split the tiled rectangle by a straight line, not crossing the interior of any tile. For example, on the picture below the tilings (a) and (b) are solid, while the tilings (c) and (d) are not.



Now the managers of the company wonder, how many different solid tilings exist for an m × n rectangle. Help them to find that out.
 

Input
The input file contains  m  and  n(1m,n16) .
 

Output
Output one integer number  mod 1e9+7 - the number of solid tilings of m×n rectangle with 2 × 1 and 1 × 2 pavement tiles.
 

Sample Input
  
  
2 2 5 6 8 7
 

Sample Output
  
  
0 6 13514
Hint
All solid tilings for the 5×6 rectangle are provided on the picture below:
 

Author
HIT
 

Source
 

Recommend
wange2014   |   We have carefully selected several similar problems for you:   5746  5745  5744  5743  5742 
 

Statistic |  Submit |  Discuss |  Note

题意:给出一个n*m大小的棋盘,需要你用1*2的砖铺满它,砖不可重叠,并且要求棋盘上没有行分割线或列分割线。

输出方法种数。需要取模。



AC代码:

#include <cstdio>

int ans[18][18]={
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,0,0,0,0,0,6,0,108,0,1182,0,10338,0,79818,0,570342}
,{0,0,0,0,0,6,0,124,62,1646,1630,18120,25654,180288,317338,1684956,3416994}
,{0,0,0,0,0,0,124,0,13514,0,765182,0,32046702,0,136189727,0,378354090}
,{0,0,0,0,0,108,62,13514,25506,991186,3103578,57718190,238225406,965022920,388537910,937145938,315565230}
,{0,0,0,0,0,0,1646,0,991186,0,262834138,0,462717719,0,560132342,0,699538539}
,{0,0,0,0,0,1182,1630,765182,3103578,262834138,759280991,264577134,712492587,886997066,577689269,510014880,807555438}
,{0,0,0,0,0,0,18120,0,57718190,0,264577134,0,759141342,0,567660301,0,47051173}
,{0,0,0,0,0,10338,25654,32046702,238225406,462717719,712492587,759141342,398579168,83006813,821419653,942235780,558077885}
,{0,0,0,0,0,0,180288,0,965022920,0,886997066,0,83006813,0,690415372,0,620388364}
,{0,0,0,0,0,79818,317338,136189727,388537910,560132342,577689269,567660301,821419653,690415372,796514774,696587391,175421667}
,{0,0,0,0,0,0,1684956,0,937145938,0,510014880,0,942235780,0,696587391,0,856463275}
,{0,0,0,0,0,570342,3416994,378354090,315565230,699538539,807555438,47051173,558077885,620388364,175421667,856463275,341279366}
};

int main()
{
    int n,m;
    while(~scanf("%d%d",&n,&m))
    {
        printf("%d\n",ans[n][m]);
    }
    return 0;
}



解题步骤:

1.最简单的轮廓线dp,求解没有任何限制情况下的铺砖方案,打表输出到文件。

#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;

#define all(x) (x).begin(), (x).end()
#define for0(a, n) for (int (a) = 0; (a) < (n); (a)++)
#define for1(a, n) for (int (a) = 1; (a) <= (n); (a)++)
#define LEFT(x)   (x<<1)
#define ysk(x)     (1<<x)
#define CLEAR(s,x)  (s&~(1<<x))
typedef long long ll;
typedef pair<int, int> pii;
const int INF =0x3f3f3f3f;
const int maxn= 16   ;
const int maxm= 16   ;
const int maxS=65536;
int no_res[maxn+5][maxn+5];
int ed,dp[2][maxS+10];
int n,m,cur;
const int mod=1e9+7.5;


void update(int a,int b)//a->b
{
   if( b&ysk(m)  )  dp[cur][CLEAR(b,m)]=(dp[cur][CLEAR(b,m)]+dp[1^cur][a])%mod;
}
int solve()
{
    if(n<m) swap(n,m);
    ed=ysk(m)-1;
    dp[0][ed]=1;
    cur=0;
    for(int i=0;i<n;i++) {
        for(int j=0;j<m;j++){
            cur^=1;
            memset(dp[cur],0,(ed+1)*sizeof dp[cur][0]);
            for(int s=0;s<=ed;s++)
            {
                //不铺
                update(s,LEFT(s));
                //上铺
                if(i&& !(s&ysk(m-1)) )    update(s,LEFT(s)|1|ysk(m)   );
                //左铺
                if(j&&  !(s&1)    )   update(s,LEFT(s)|3    );

            }
        }
    }
    return dp[cur][ed];
}


void get_no_res()
{
   for(int i=0;i<=maxn;i++){
    for(int j=i;j<=maxm;j++){
        if(!i||!j)  {no_res[i][j]=no_res[j][i]=1;continue;}
        n=i,m=j;
        no_res[j][i]=no_res[i][j]=solve();
    }
   }

}
int main()
{
   get_no_res();
   freopen("no_res.txt","w",stdout);
   printf("int no_res[%d][%d]={\n",18,18);
   for0(i,maxn+1)
   {
       if( i )  putchar(',');
       putchar('{');
       for0(j,maxm+1)
       {
           if(j) putchar(',');
           printf("%d",no_res[i][j]);
       }
       putchar('}');
       putchar('\n');
   }
   printf("};\n");
   return 0;
}






2.容斥原理

对于n*m的棋盘,求解不存在行分割线和列分割线的情况数ans(n,m)。

很容易想到用容斥原理,但确实是不怎么好用。

有个很自然的想法是对列或行进行容斥,加加减减。

先不考虑行分割。

(容斥原理的过程:一开始认为ans(n,m)就是no_res(n,m),但是不对,所求值比实际值大,因为no_res(n,m)是没有限制的情况,可能会有列分割。

这个时候需要减去有列分割的情况,先考虑至少有一列分割的情况,

将m拆成两部分a,b,答案减去至少有一列的所有拆法。之后减多了,再把至少有两列的拆法加上去,...,+,-+,-...)


大体思路:

用状压枚举列分割的位置,然后不同情况做相应的加减计数。


上述的容斥原理正确的条件是,都没有行分割线。


令dp[x]代表求解当前列宽m时,高度为x的矩形(x*m)  限制后的铺法种数(没有行分割线,但是要满足恰好具有当前枚举的列分割线)。


dp[n]是可递推求解的 ,容易求出dp[1],

n行对应n+1个点 0,1,2,...,n。

首先令dp[n]=no_res(n,m),代表可以有行分割线,之后再想办法减去有行分割线的情况。


如果计算的顺序得当就可以很好的解决重复问题,

dp[n]=dp[n]-dp[1]*( 前面1行内不许出现行分割线,剩余n-1行不作限制要满足当前列分割线的种数  )-dp[2]*(前面2行内不许出现行分割线剩余n-2行不作限制要满足当前列分割线的种数)-...

就是说先减去有最上面一条分割线的情况,再减去有它下面一条分割线的情况(因为这时候dp[2]保证了上面行的分割线都不出现,所以不会重复),再减去存在下面的一条行分割线的情况....。



具体见代码


#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;

#define all(x) (x).begin(), (x).end()
#define for0(a, n) for (int (a) = 0; (a) < (n); (a)++)
#define for1(a, n) for (int (a) = 1; (a) <= (n); (a)++)
#define ysk(x)     (1<<x)
typedef long long ll;
typedef pair<int, int> pii;
const int INF =0x3f3f3f3f;
const int maxn= 16   ;
const int maxm= 16   ;
const int mod=1e9+7.5;
int ans[maxn+3][maxm+3];
int n,m;
int no_res[18][18]={
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
,{1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1}
,{1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597}
,{1,0,3,0,11,0,41,0,153,0,571,0,2131,0,7953,0,29681}
,{1,1,5,11,36,95,281,781,2245,6336,18061,51205,145601,413351,1174500,3335651,9475901}
,{1,0,8,0,95,0,1183,0,14824,0,185921,0,2332097,0,29253160,0,366944287}
,{1,1,13,41,281,1183,6728,31529,167089,817991,4213133,21001799,106912793,536948224,720246619,704300462,289288426}
,{1,0,21,0,781,0,31529,0,1292697,0,53175517,0,188978103,0,124166811,0,708175999}
,{1,1,34,153,2245,14824,167089,1292697,12988816,108435745,31151234,940739768,741005255,164248716,498190405,200052235,282756494}
,{1,0,55,0,6336,0,817991,0,108435745,0,479521663,0,528655152,0,764896039,0,416579196}
,{1,1,89,571,18061,185921,4213133,53175517,31151234,479521663,584044562,472546535,732130620,186229290,274787842,732073997,320338127}
,{1,0,144,0,51205,0,21001799,0,940739768,0,472546535,0,177126748,0,513673802,0,881924366}
,{1,1,233,2131,145601,2332097,106912793,188978103,741005255,528655152,732130620,177126748,150536661,389322891,371114062,65334618,119004311}
,{1,0,377,0,413351,0,536948224,0,164248716,0,186229290,0,389322891,0,351258337,0,144590622}
,{1,1,610,7953,1174500,29253160,720246619,124166811,498190405,764896039,274787842,513673802,371114062,351258337,722065660,236847118,451896972}
,{1,0,987,0,3335651,0,704300462,0,200052235,0,732073997,0,65334618,0,236847118,0,974417347}
,{1,1,1597,29681,9475901,366944287,289288426,708175999,282756494,416579196,320338127,881924366,119004311,144590622,451896972,974417347,378503901}
};



int bitsize[20];
int dp[20];


int solve()
{
   int tot=0;
   int ted=ysk(m-1)-1;
   for(int s=0;s<=ted;s++  )//枚举列分割线位置
   {
       int nbit=0,last=-1;//nbit代表被划分为了多少块
       for(int i=0;i<m-1;i++) if(s&ysk(i))
       {
           bitsize[nbit++]=i-last;//bitsize[i]指的是序号为i的块的宽度
           last=i;
       }
       bitsize[nbit++]=m-1-last;

       for(int i=1;i<=n;i++)
       {
           for(int j=0;j<i;j++)//[0,j]没有行分割线(但要满足列分割条件),[j,n]不限制,但要满足列分割条件
           {
                ll reg=1;
                for(int k=0;k<nbit;k++)
                {
                   reg=reg*no_res[i-j][bitsize[k]  ]%mod;//[j,n]区域乘法原理
                }
                if(!j)  dp[i]=reg;
                else
                {
                    reg=(reg*dp[j])%mod;//[0,j]区域,利用已经计算的dp值递推。
                    dp[i]=((dp[i]-reg)%mod+mod)%mod;
                }
           }
       }

       if(nbit&1)  tot=(tot+dp[n])%mod;//容斥原理的偶加,注意这里nbit是奇数,那么列分割线则为偶数条
       else        tot=((tot-dp[n])%mod+mod)%mod;//奇减

   }




   return tot;
}

int main()
{

   for(int i=1;i<=maxn;i++) {
     for(int j=i;j<=maxn;j++){
            n=i,m=j;
            ans[i][j]=ans[j][i]=solve();
     }
   }

   freopen("ans.txt","w",stdout);
   printf("int ans[%d][%d]={\n",18,18);
   for0(i,maxn+1)
   {
       if( i )  putchar(',');
       putchar('{');
       for0(j,maxm+1)
       {
           if(j) putchar(',');
           printf("%d",ans[i][j]);
       }
       putchar('}');
       putchar('\n');
   }
   printf("};\n");


   return 0;
}






附上一个很慢的方法,上面的方法是看别人的代码弄懂的,我自己的做法打表跑了40分钟:


那就是对列容斥,就是说dfs哪些位置有列分隔线,然后再进行轮廓线dp,使得既没有行分割线,又满足了枚举的列分割线:

具体做法是对普通的铺砖问题稍作变换,

如果现在正考虑铺设某一行的第一个格子(第一行除外),那么轮廓线表示的正好应该是一整行的格子情况,

如果轮廓线上的格子全满,证明有行分割线,continue;

如果现在正考虑铺设某一行的第一个格子(第二行除外),那么轮廓线表示的正好应该是一整行的格子情况,

如果轮廓线上的格子全空,证明有行分割,continue;


保证哪些位置有列分割线更简单:就是如果这里有列分割线,那么不能有砖跨过这条线(针对于横铺才有可能跨过)。



#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;

#define all(x) (x).begin(), (x).end()
#define for0(a, n) for (int (a) = 0; (a) < (n); (a)++)
#define for1(a, n) for (int (a) = 1; (a) <= (n); (a)++)
#define LEFT(x)   (x<<1)
#define ysk(x)     (1<<x)
#define CLEAR(s,x)  (s&~(1<<x))
typedef long long ll;
typedef pair<int, int> pii;
const int INF =0x3f3f3f3f;
const int maxn=16    ;
const ll mod=1e9+7.5 ;
const int maxS=65536;
int ans_tmp,d[2][maxS+2],ans[maxn+10][maxn+10];
int ed,now;
bool res[maxn+10];
int n,m;
bool ca[maxn+10][maxn+10];
void update(int a,int b)
{
   if(b&ysk(m)) d[now][CLEAR(b,m)]= (d[now][CLEAR(b,m)]+d[1^now][a])%mod;
}



void show(bool *res)
{
    for(int i=0;i<m;i++)
    {
        if(res[i] )putchar('1');
        else  putchar('0');
    }
    putchar('\n');
}

ll work()
{
   d[0][ed]=1;
   now=0;
   for0(i,n) {
      for0(j,m){
         now^=1;
         memset(d[now],0,(ed+1)*sizeof d[now][0]);
         for(int s=0;s<=ed;s++) {
                /*如果发现在计算某一行的开头一个格子时,
            轮廓线上全部填满,或者未填,不能转移,这样保证了没有行分割线
            但是要注意 全满对于最上面一行(第1行)不成立,全空对于正在计算第2行时不成立
            */
            if(i&&j==0&&s==ed)  continue;
            if(i!=1&&j==0&&s==0)  continue;
            if(d[now^1][s]==0)  continue;//剪枝

            update( s ,LEFT(s) ); //不放

            if(j>0&& !(s&1)  )//左放
            {
                if(  !res[j-1]   )//如果左边这条线必须是列分割线,那么不能横放。
                update( s ,LEFT(s)|3 );

            }
            if(i>0&& !(s&ysk(m-1))  )//上
            {
                update(  s ,LEFT(s)|ysk(m)|1   );
            }

         }

      }
   }
   return d[now][ed];

}

void cal(int neg)
{

    for(int j=0;j<m;j++)  if(res[j])
    {
        int x= (j+1)*n;
        int y=n*m-x;
        if( (x&1) ||  (y&1) )  return;//如果分割线两端的格子数为奇数,那么一定无解,剪枝。

    }
    int ret=neg*work();

    ans_tmp=ans_tmp+ret;
    ans_tmp=(ans_tmp%mod+mod)%mod;


}


void dfs(int col,int neg)//dfs容斥原理,neg代表正负号
{
    if(col==m-1){
        cal(neg);
        return;
    }
    res[col]=0;
    dfs(col+1,neg);
    res[col]=1;
    dfs(col+1,-neg);


}
void solve()
{
    ed=ysk(m)-1;//轮廓线上的最大状态

    memset(res,0,sizeof res);

    ans_tmp=0;
    dfs(0,1);
    ans[n][m]=ans[m][n]=ans_tmp;
    ca[n][m]=ca[m][n]=1;
    printf("ans[%d][%d]=%d\n",n,m,ans[n][m]);


}

void debug()
{
    n=8,m=7;
    solve();

}

int main()
{
//    debug();
   freopen("output.txt","w" ,stdout);
    memset(ca,0,sizeof ca);//都没有计算过
    for1(i,maxn)  for1(j,maxn) if(!ca[i][j])
    {
        n=i,m=j;
        if( (i*j)%2 )  {ans[i][j]=ans[j][i]=0;ca[i][j]=ca[j][i]=1;continue;}
        if(n<m)  swap(n,m);//这个swap()很重要,不然更慢
        solve();
    }


//打表出奇迹
    printf("ans[16][16]={");
    bool put=0;
    for1(i,maxn){

       if(put)  putchar(',');
        put=1;
        printf("{");
       for1(j,maxn){
           if(j!=1)  putchar(',');
           printf("%d",ans[i][j]);
       }
       printf("}");
    }
    printf("};");

   return 0;
}




  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值