hihocoder 1048

题意:在一个N×M的格子里,现在有两种类型的砖块,1×2和2×1,问一共有多少种方案,可以将整个N×M的空间都填满。2<=N<=1000, 3<=m<=5。

思路:参考博客:http://blog.csdn.net/lu597203933/article/details/44137277

(事实上考虑的时候顺序是这样的1⃣️👉4⃣️👉3⃣️👉2⃣️)

1⃣️确定0表示什么,1表示什么。

在位置(i, j)如果我们选择横着贴砖,那么将(i, j),(i, j+1)都填写成1,如果竖着贴砖,我们将(i,j)填写成0,将(i+1, j)填写成1。

为什么这样表示呢?

首先确定有几种情况:

假如现在我们在铺砖位置(i, j), 并且假设之前的位置已经铺设好的了,在这个位置,我们的选择:

1.不用铺了,(i-1,j)竖着铺好了,那就继续看(i,j+1);

2.横着铺,(i,j)和(i,j+1)都铺上;

3.竖着铺,(i,j)和(i+1,j)都铺上;

那么为什么竖着铺的时候(i,j)填写成0,(i+1, j)填写成1呢?

假设反过来,会造成什么结果呢?

如果某一位是0,那么你就看上一块是不是1即可;如果某一位是1,要检查右边那块是不是1,如果不是1,说明是竖着放的,向右继续检查,如果是1,说明可能是横着放的也有可能是竖着放的,假设是横着放的,那要判断右边那块的上面一块是不是1,但是这里就出现矛盾了,这里的1,到底是指竖着放的的一块呢还是指横着放的某一块呢?

所以要把(i,j)填写成0,(i+1, j)填写成1。

还有另一种解释:横着放设为1表示对下一行没有任何影响了,竖着放的第一块,对下面是有影响的,所以为0,第二块则是没有影响的,为1。

这样选择还有一个好处:在处理最后一行的时候,可以保证最后一行都是1, 因为最后一行绝对不能成为竖砖开始,所以很容易取得最后的解。

2⃣️确定第一行的合法状态。

枚举所有状态,对于每个状态的每一列进行判断。

如果某一位是0,就继续向右检查;

如果某一位是1,这时是不可能作为竖着放的第二块的,只可能是横着放的,那么就检查右边那块是不是1,这里注意不要越界,如果是1,就j- -,否则就不合法。(这里的判断的(i,x)一定不是由(i,x-1)位横着铺砖过来的,否则直接x=x+2,就不会来判断(i,x)位了)

注意⚠️:判断某一位是0还是1时,要注意如果是1的话结果应该是>0而不是1本身,因为这一位是有权值的。

void test1()
{
    for(int i=0;i<(1<<m);i++)
    {
        int sign=0;
        for(int j=m-1;j>=0&&sign==0;j--)
        {
            if(((1<<j)&i)>0)//该位是1,则后一块也得是1,第一行的1只能是横着放的
            {
                if(j==0)sign++;
                else if(((1<<(j-1))&i)==0)sign++;
                else j--;
            }
        }
        if(sign==0)dp[1][i]=1;
    }
}

3⃣️动态规划。

dp(i)(j):推到第i行状态为j时的方法数。

dp(i)(j)=sum{dp(i-1)(k),0<=k<(1<<m)&&k和j是可以兼容的}

判断是否兼容:

如果某一位是0,那么上一块只能是1;

如果某一位是1,那么就分两种情况:

上一块是0,那么继续向右检测;

上一块是1,那么就说明这里是横着铺的,右边一块也一定是1,右边一块的上一块一定是1,因为右边一块已经确定状态是横着铺了,如果都满足的话,就j- -,否则就说明不兼容。

bool judge(int x,int y)//x:这一行的状态,y:上一行的状态
{
    for(int i=m-1;i>=0;i--)
    {
        if(((1<<i)&x)==0)//该位是0,则上一块只能是1
        {
            if(((1<<i)&y)==0)
                return false;
        }
        else//该位是1
        {
            if(((1<<i)&y)>0)//如果上一块是0,就继续检测;如果上一块是1,(i,x+1)也一定是1,****并且(i-1,x+1)一定是1!!!(注意!!!)
            {
                if(i==0)return false;
                else if(((1<<(i-1))&x)>0&&((1<<(i-1))&y)>0)
                    i--;
                else return false;
            }
        }
    }
    return true;
}
for(int i=2;i<=n;i++)//每一行
{
    for(int j=0;j<(1<<m);j++)//这一行的状态
    {
        for(int k=0;k<(1<<m);k++)//上一行的状态
        {
            if(judge(j,k))
                dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
        }
    }
}

4⃣️dp(n)((1<<m)-1)即为总方法数。

因为最后一行只能都为1。

完整代码:

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<time.h>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<set>
#include<map>
#include<vector>
#include<algorithm>
#define INF 0x3f3f3f3f
#define MOD 1000000007
using namespace std;
long long dp[1100][(1<<5)+10];//在第i行状态为j时的方法数
//竖放的第一块是0,第二块是1
int m;
void test1()
{
    for(int i=0;i<(1<<m);i++)
    {
        //printf("i=%d:",i);
        int sign=0;
        /*for(int j=m-1;j>=0;j--)
            printf("%d",((1<<j)&i));*/
        for(int j=m-1;j>=0&&sign==0;j--)
        {
            if(((1<<j)&i)>0)//该位是1,则后一块也得是1,第一行的1只能是横着放的
                //这里是>0,注意!!!不一定是1呀!!!
            {
                if(j==0)sign++;
                else if(((1<<(j-1))&i)==0)sign++;
                else j--;
            }
        }
        if(sign==0)dp[1][i]=1;
        //printf(" %lld\n",dp[1][i]);
    }
    //printf("\n");
}
bool judge(int x,int y)//x:这一行的状态,y:上一行的状态
{
    for(int i=m-1;i>=0;i--)
    {
        if(((1<<i)&x)==0)//该位是0,则上一块只能是1
        {
            if(((1<<i)&y)==0)
                return false;
        }
        else//该位是1
        {
            if(((1<<i)&y)>0)//如果上一块是0,就继续检测;如果上一块是1,(i,x+1)也一定是1,****并且(i-1,x+1)一定是1!!!(注意!!!)
            {
                if(i==0)return false;
                else if(((1<<(i-1))&x)>0&&((1<<(i-1))&y)>0)
                    i--;
                else return false;
            }
        }
    }
    return true;
}
int main()
{
    int n;//m比较小
    scanf("%d%d",&n,&m);
    memset(dp,0,sizeof(dp));
    test1();//处理出第一行合法的情况
    for(int i=2;i<=n;i++)//每一行
    {
        for(int j=0;j<(1<<m);j++)//这一行的状态
        {
            for(int k=0;k<(1<<m);k++)//上一行的状态
            {
                if(judge(j,k))
                    dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
            }
            //printf("dp[%d][%d]:%lld\n",i,j,dp[i][j]);
        }
    }
    printf("%lld\n",dp[n][(1<<m)-1]);//最后一行是不可能出现0的
    return 0;
}

这里用枚举+判断会有些慢…

所以可以直接生成合法的方案。

1⃣️0和1的表示如上一个方法。

2⃣️对于每一个位置,有三种放置方法。

不放(下放):这时这个块为0,则上一块只能是1。

上放:这时这个块为1,则上一块只能是0。

横放:这时这个块为1,右边的块也为1,那么上面那块为1,右边的上面那块也为1。

然后从0开始构造即可。

int num;//兼容的方案数
int ok[50010][2];//ok[][0]:上一行的状态;ok[][1]:下一行的状态
void dfs(int c,int pre,int now)//构造出所有兼容模式:c(列数),pre(上一行的状态),pre(该行的状态)
{
    if(c>m)return;//防止横放越界的情况
    if(c==m)
    {
        ok[num][0]=pre;
        ok[num++][1]=now;
        return;
    }
    dfs(c+1,(pre<<1)|1,now<<1);//不放(即下放),为0,则上行为1
    dfs(c+1,pre<<1,(now<<1)|1);//上放,为1,则上行为0
    dfs(c+2,(pre<<2)|3,(now<<2)|3);//横放,为1,右边为1,上边为1,上边的右边为1
}

3⃣️动态规划。

long long dp[15][50010];//推到第i行兼容方式为j时方法数
memset(dp,0,sizeof(dp));
dp[0][(1<<m)-1]=1;//第一行不能上放,所以第0行不能有空的,只能为1
for(int i=1;i<=n;i++)
{
    for(int j=0;j<num;j++)
    {
        dp[i][ok[j][1]]+=dp[i-1][ok[j][0]];
    }
}

因为第一行不能上放,所以第0行不能有0,只能都是1。

dp(i)(j):推到第i行状态为j时的方法数。

dp(i)(j)=sum{dp(i-1)(k),0<=k<(1<<m)&&k和j是可以兼容的}。

4⃣️dp(n)((1<<m)-1)即为答案。

完整代码:

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<time.h>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<set>
#include<map>
#include<vector>
#include<algorithm>
#define INF 0x3f3f3f3f
#define MOD 1000000007
using namespace std;
int m;
int num;//兼容的方案数
int ok[50010][2];//ok[][0]:上一行的状态;ok[][1]:下一行的状态
long long dp[15][50010];//推到第i行兼容方式为j时方法数
void dfs(int c,int pre,int now)//构造出所有兼容模式:c(列数),pre(上一行的状态),pre(该行的状态)
{
    if(c>m)return;//防止横放越界的情况
    if(c==m)
    {
        ok[num][0]=pre;
        ok[num++][1]=now;
        return;
    }
    dfs(c+1,(pre<<1)|1,now<<1);//不放(即下放),为0,则上行为1
    dfs(c+1,pre<<1,(now<<1)|1);//上放,为1,则上行为0
    dfs(c+2,(pre<<2)|3,(now<<2)|3);//横放,为1,右边为1,上边为1,上边的右边为1
}
int main()
{
    int n;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        if(n==0&&m==0)break;
        num=0;
        dfs(0,0,0);
        //printf("%d\n",num);
        memset(dp,0,sizeof(dp));
        dp[0][(1<<m)-1]=1;//第一行不能上放,所以第0行不能有空的,只能为1
        for(int i=1;i<=n;i++)
        {
            for(int j=0;j<num;j++)
            {
                dp[i][ok[j][1]]+=dp[i-1][ok[j][0]];
            }
        }
        printf("%lld\n",dp[n][(1<<m)-1]);
    }
    return 0;
}

当n很大的时候,就不能用上述的方法算了,这个时候矩阵的优势就体现出来了。

51nod 1033

题意:2 <= m <= 10^9,2 <= n <= 5

思路:题目转化为求经过n次转化,从初始态到终态有几种转化法。

建立矩阵的方法:矩阵的大小为(1<<m-1) ,如果pre状态能到达状态now,那么mat(pre)(now)=1,然后求此矩阵的n次幂即可。

输出的是ans((1<<m)-1)((1<<m)-1),因为第0行都为1,最后一行也都为1。

完整代码:

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<math.h>
#include<time.h>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<set>
#include<map>
#include<vector>
#include<algorithm>
#define INF 0x3f3f3f3f
#define MOD 1000000007
using namespace std;
int m;
struct mat
{
    long long a[40][40];
}mt,r;
void dfs(int c,int pre,int now)//构造出所有兼容模式:c(列数),pre(上一行的状态),pre(该行的状态)
{
    if(c>m)return;//防止横放越界的情况
    if(c==m)
    {
        mt.a[pre][now]=1;
        return;
    }
    dfs(c+1,(pre<<1)|1,now<<1);//不放(即下放),为0,则上行为1
    dfs(c+1,pre<<1,(now<<1)|1);//上放,为1,则上行为0
    dfs(c+2,(pre<<2)|3,(now<<2)|3);//横放,为1,右边为1,上边为1,上边的右边为1
}
mat mult(mat x,mat y)
{
    mat res={0};
    int i,j,k;
    for(i=0;i<(1<<m);i++)
        for(j=0;j<(1<<m);j++)
            for(k=0;k<(1<<m);k++)
                res.a[i][j]=(res.a[i][j]+(x.a[i][k]*y.a[k][j])%MOD)%MOD;
    return res;
}
mat PowerMod(mat x,long long n)
{
    mat ans={0};
    int i;
    for(i=0;i<(1<<m);i++)
        ans.a[i][i]=1;
    while(n>0)
    {
        if(n%2==1)
            ans=mult(ans,x);
        x=mult(x,x);
        n=n/2;
    }
    return ans;
}
int main()
{
    long long n;
    scanf("%lld%d",&n,&m);
    memset(mt.a,0,sizeof(mt.a));
    dfs(0,0,0);
    r=PowerMod(mt,n);
    printf("%lld\n",r.a[(1<<m)-1][(1<<m)-1]);//第0行只能全是1
    return 0;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

树形dp Previous
终于搭建了我的博客啦www Next