AtCoder Beginner Contest 204 F - Hanjo 2(状压DP + 矩阵快速幂加速)

15 篇文章 0 订阅
5 篇文章 0 订阅

F - Hanjo 2

题意

给出不限数量的 1 ∗ 1 1 *1 11 1 ∗ 2 1 * 2 12 的方块,要求填满 n ∗ m n * m nm 的空间的方案数。其中 n < = 6 , m < = 1 0 12 n <= 6,m<=10^{12} n<=6,m<=1012

解析

本题和 Acwing 的状压DP题很像, 蒙德里安的梦想

我们先不考虑巨大的第二维,先考虑状态和转移,根据蒙德里安的梦想的dp状态定义: f i j : f_{ij}: fij: j j j 代表的是二进制,而二进制数位上的 1 1 1 代表第 i i i 列的该行有一个方块是横着放置伸展到第 i + 1 i + 1 i+1,蒙德里安没有 1 ∗ 1 1 * 1 11 的方块所以要求剩余的连续空位必须是偶数,且方案唯一。但我们无论当前是怎样的形状都能用 1 ∗ 1 1 * 1 11 的方块去填补剩余的空位,所以我们无须讨论放置方案是否合法而是需要计算放置 1 ∗ 1 1 * 1 11 1 ∗ 2 1 * 2 12 方案数。

我们方程的入口就是 f 00 = 1 f_{00} = 1 f00=1,即第 0 0 0 列不存在一个 1 ∗ 2 1 * 2 12 的方块伸展到 1 1 1 列,方程的出口就是 a n s = f m 0 ans = f_{m0} ans=fm0 同样是没有一个 1 ∗ 2 1 * 2 12 的方块伸到第 m + 1 m + 1 m+1 列的方案。

那么这里先给出 DP 方程转移,以及方案计算,有详细的代码注释。

bool check(int x, int y){
    for(int i = 0; i < n; i ++)
        if((x >> i & 1) && (y >> i & 1)) return false; // 前一列的第i行有横着放的,那么这列的这一行就不能有横着放
    return true;
}

int c[10] = {1, 1, 2, 3, 5, 8, 13}; // c[i]:长度为i的连续空位,随意放1*1和1*2方块并填满的方案数
int get_res(int x, int y){ // 放竖着放1*2和1*1的方案数
    int sum = 0, res = 1;
    for(int i = 0; i < n; i ++){
        if(!(x >> i & 1) && !(y >> i & 1)) sum ++; // 前一列第i行没有横着放,以及本列第i行也没有横着放就是空位,可以考虑放1*1/1*2
        else res *= c[sum], sum = 0;
    }
    res *= c[sum];
    return res;
}

void dp(){ // 具体可以参考Acwing的蒙德里安的梦想,只是稍作修改
    ll f[110][110] = {0}; // 二进制上的 1 代表 第 i 列的 该行是否有一个横着放的2*1方块,剩余空位可以放1*1和1*2
    f[0][0] = 1;
    for(int i = 1; i <= m; i ++){
        for(int j = 0; j < (1 << n); j ++){
            for(int k = 0; k < (1 << n); k ++){
                if(check(j, k)){
                    f[i][k] = (f[i][k] + f[i - 1][j] * get_res(j, k) % mod) % mod;
                }
            }
        }
    }   
    cout << f[m][0] % mod;
    return ; 
}

我们现在已经写出DP状态和方程转移,但是这样转移是 O ( m ∗ 2 2 n ) O(m*2^{2n}) O(m22n) 矩阵巨大的第二维 m < = 1 0 12 m<=10^{12} m<=1012,告诉我们肯定需要优化。

观察每次的转移,发现每个第 i i i 列到第 i + 1 i + 1 i+1 列之间的转移都是相同的,唯一变化只是第 i i i 列的值。 于是我们就想到用矩阵快速幂来加速这一过程。具体看代码以及注释。

完整代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int SIZ = 100, mod = 998244353;

ll n, m;

bool check(int x, int y){
    for(int i = 0; i < n; i ++)
        if((x >> i & 1) && (y >> i & 1)) return false; // 前一列的第i行有横着放的,那么这一行就不能有横着放
    return true;
}

int c[10] = {1, 1, 2, 3, 5, 8, 13}; // c[i]:长度为i的连续空位,随意放1*1和1*2方块并填满的方案数
int get_res(int x, int y){ // 放竖着放1*2和1*1的方案数
    int sum = 0, res = 1;
    for(int i = 0; i < n; i ++){
        if(!(x >> i & 1) && !(y >> i & 1)) sum ++; // 前一列第i行没有横着放,以及本列第i行也没有横着放就是空位,可以考虑放1*1/1*2
        else res *= c[sum], sum = 0;
    }
    res *= c[sum];
    return res;
}

void dp(){ // 具体可以参考Acwing的蒙德里安的梦想,只是稍作修改
    ll f[110][110] = {0}; // 二进制上的 1 代表 第 i 列的 该行是否有一个横着放的2*1方块,剩余空位可以放1*1和1*2
    f[0][0] = 1;
    for(int i = 1; i <= m; i ++){
        for(int j = 0; j < (1 << n); j ++){
            for(int k = 0; k < (1 << n); k ++){
                if(check(j, k)){
                    f[i][k] = (f[i][k] + f[i - 1][j] * get_res(j, k) % mod) % mod;
                }
            }
        }
    }   
    cout << f[m][0] % mod;
    return ; 
}

struct Matrix{
    ll Mat[SIZ][SIZ];
    Matrix(){
        for(int i = 0; i < (1 << n); i ++)
            for(int j = 0; j < (1 << n); j ++) Mat[i][j] = 0;
    }
    inline void build(){
        for(int i = 0; i < (1 << n); i ++) Mat[i][i] = 1;
    }
};

Matrix operator *(const Matrix &A, const Matrix & B){ // 重载矩阵乘法
    Matrix a;
    for(int i = 0; i < (1 << n); i ++){
        for(int j = 0; j < (1 << n); j ++){
            for(int k = 0; k < (1 << n); k ++){
                a.Mat[i][j] = (a.Mat[i][j] + A.Mat[i][k] * B.Mat[k][j] % mod) % mod;
            }
        }
    }
    return a;
}

Matrix ksm(Matrix a, ll b){ // 矩阵快速幂加速递推
    Matrix res; res.build();
    while(b){
        if(b & 1) res = res * a;
        a = a * a;
        b >>= 1;
    }
    return res;
}

int main(){
    cin >> n >> m;

    Matrix A, B;
    for(int i = 0; i < (1 << n); i ++){ // 因为每次转移的都是相同的,根据dp方程式转移来构造矩阵 参考dp()
        for(int j = 0; j < (1 << n); j ++){
            if(check(i, j)){
                A.Mat[j][i] = get_res(i, j); // 可以从状态 i -> j 并且 fj + fi * res ,所以矩阵系数为res
            }
        }
    }

    B.Mat[0][0] = 1; // 初始矩阵 即 f[0][0] = 1

    Matrix ans = ksm(A, m); // 从 0 -> m 需要转移 m 次 m 次幂
    B = ans * B; // 最后乘上初始矩阵

    cout << B.Mat[0][0];
    return 0;
}

提交记录:Submission #40530870

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值