软件能力认证题---拼图(状态压缩DP+矩阵快速幂)

题意: 给定n*m的棋盘(1<=N<=10^15, 1<=M<=7),用L型骨牌(田字型任意去掉一个口)完全覆盖它,问有多少种解。

思路:m的范围只有1<=M<=7,显然状压DP。但是N的最大值到10^15,只能用快速幂了。

状态表示:0代表此处留空,1代表此处被填满。01序列压缩成一个int型来表示一行的填放情况。(例如:状态为4,则代表100,即第一列填满,第二第列三空)

递推矩阵是长这样的:

边界条件:

其中,

t = 2^M

代表将前i-1行填满,且第i行放置了状态s时的总方案数。

代表上一行原本放置了状态s2的前提下,当前行放置骨牌把上一行填满并使得当前行状态为s1的方案数。


那么问题来了,怎么取得矩阵D呢?

我枚举上一行状态s2,对每个s2进行dfs:依次扫描s2的每一位,如果有空位置,则尝试拜访某个方向的骨牌(显然共4种方向)。当把s2所有位置拜满以后,s1便产生了,那么让增加1 (初始为0).


有了矩阵D,天黑都不怕!

显然有


矩阵快速幂即可,最后输出就是结果。

题目本身没多难,都怪我装压DP没学好,做了这题总算弄明白了一些误区。

明明还有好多事情要忙,但是看到这种东西就是想做,好久没有这种纯粹的感觉了。。


下面附上代码

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;

typedef long long LL;
const int mod = 1000000007;
const int maxn = 130;
int off[5]={0,1,1,2,2};
int d[maxn][maxn];
LL N;
int M; //N行 M列
int maxs; //总状态数 1<<M

inline void int2arr(int num,bool arr[])//数字转矩阵
{
    for(int i=0;i<M;++i,num>>=1)
        arr[M-i-1] = num&1;
}

inline int arr2int(bool arr[])//矩阵转数字
{
    int num = 0;
    for(int i=0;i<M;(num<<=1)|=arr[i++]);
    return num;
}

bool sbuf[2][10];
inline bool check(int cur,int type)//判断在cur位置是否可以放置type方向的骨牌
{
    if(type == 1)
        return sbuf[0][cur]==0 && sbuf[1][cur]==0 && cur+1<M && sbuf[1][cur+1]==0;

    if(type == 2)
        return sbuf[0][cur]==0 && sbuf[1][cur]==0 && cur-1>=0 && sbuf[1][cur-1]==0;

    if(type == 3)
        return sbuf[0][cur]==0 && sbuf[1][cur]==0 && cur+1<M && sbuf[0][cur+1]==0;

    if(type == 4)
        return sbuf[0][cur]==0 && cur+1<M && sbuf[0][cur+1]==0 && sbuf[1][cur+1]==0;

}

inline void putblock(int cur,int type,int cont)//在cur位置放置type方向的骨牌
{
    if(type == 1)
        sbuf[1][cur] = sbuf[1][cur+1] = cont;

    if(type == 2)
        sbuf[1][cur] = sbuf[1][cur-1] = cont;

    if(type == 3)
        sbuf[1][cur] = cont;

    if(type == 4)
        sbuf[1][cur+1] = cont;
}

void dfs(int cur)//dfs不多解释
{
    if(cur >= M)
    {
        ++d[arr2int(sbuf[1])][arr2int(sbuf[0])];
        return;
    }

    if(sbuf[0][cur]==1) dfs(cur+1);//如果当前位置已填,继续尝试下一列

    for(int i=1;i<=4;++i)
        if(check(cur,i))//如果当前位置可以放置i方向骨牌
        {
            putblock(cur,i,1);//尝试放置i方向骨牌
            dfs(cur+off[i]);//继续尝试后面列
            putblock(cur,i,0);//撤销放置i方向骨牌
        }
}

inline void getd()//计算矩阵D
{
    maxs = 1<<M;
    memset(d,0,sizeof(d));
    for(int i=0;i<maxs;++i)
            int2arr(i,sbuf[0]),
            dfs(0);

}

inline void matmult(int a[maxn][maxn],int b[maxn][maxn])//矩阵乘法a*=b
{
    static int c[maxn][maxn];
    memset(c,0,sizeof(c));
    for(int i=0;i<maxs;++i)
        for(int j=0;j<maxs;++j)
            for(int k=0;k<maxs;++k)
                c[i][j] = (c[i][j] + ((LL)a[i][k] * (LL)b[k][j]) % mod) % mod;

    memcpy(a,c,sizeof(c));
}

void matpower(int a[maxn][maxn],LL p)//矩阵快速幂a^p
{
    int ans[maxn][maxn];
    memset(ans,0,sizeof(ans));
    for(int i=0;i<maxs;++i) ans[i][i]=1;    //ans=1

    for(;p;p>>=1,matmult(a,a))
        if(p&1)
            matmult(ans,a);//ans*=a;
       //a*=a;
    memcpy(a,ans,sizeof(ans)); //return ans
}

int main()
{
    cin>>N>>M;
    getd();
    matpower(d,N);
    cout<< d[ maxs-1 ][ maxs-1 ] <<endl;
    return 0;
}

/*
各个方向的骨牌及其对应编号
 1  *
    **

 2  *
   **

 3  **
    *

 4  **
     *
*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值