[ZJOI2019] 麻将

文章介绍了如何运用动态规划和自动机来解决一个关于麻将胡牌策略的数学问题。首先定义了状态表示,然后构建自动机预处理可能的状态,并通过外层动态规划进行状态转移计算。最后,通过爆搜和优化时间复杂度,得出解决方案。
摘要由CSDN通过智能技术生成

首先发现一个集合怎么判定就不太简单,先考虑判定!

判掉 ≥ 7 \ge 7 7 个对子的情况,考虑另外一种胡牌,设 f ( 0 / 1 , j , k ) f(0/1,j,k) f(0/1,j,k) 表示此时无 / 有用来胡牌的对子,有 j j j 个形如 i − 1 , i i-1,i i1,i 的不完整面子, k k k i i i 的单牌,最多能组出多少个面子。其实和 NOI2022 D1T2 那个 dp套dp 题的内层状态设计很像。

判定条件就是存在一个 f ( 1 , j , k ) ≥ 4 f(1,j,k)\ge 4 f(1,j,k)4

考虑计数。也就是统计选了 i ( i ∈ [ 0 , 4 n − 13 ] ) i(i\in [0,4n-13]) i(i[0,4n13]) 张牌还没有胡牌的方案数。

考虑 dp套dp,这个可以感性猜测一下,胡牌的时候牌数并不会很多,所以有可能还没胡牌的集合非常少。建出来自动机看看有多少个节点就是。这一步就是爆搜。

先把所有内层 dp 可能达到的状态用 bfs 预处理成一个自动机的模型,外层再套一个 dp:设 d p ( i , j , k ) dp(i,j,k) dp(i,j,k) 表示考虑完前 i i i 种牌,现在选了 j j j 张牌,处在自动机节点 k k k 上的方案数。转移是简单的,枚举第 i i i 种牌选了几张,自动机节点跟着转移即可。

时间复杂度大概是 O ( ∣ S ∣ × n 2 ) \mathcal O(|\mathcal S|\times n^2) O(S×n2),其中 S \mathcal S S 表示自动机的结点集合。搜出来是 2092 个。确实不多。可以过。

// Problem: P5279 [ZJOI2019]麻将
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5279
// Memory Limit: 500 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>

const int maxn = 405;
const int maxm = 2205;
const int mod = 998244353;

void add(int& x, int y) {
    if((x += y) >= mod)
        x -= mod;
    return ;
}

void sub(int& x, int y) {
    if((x -= y) < 0)
        x += mod;
    return ;
}

int inc(int x, int y) {
    return (x + y) >= mod ? (x + y - mod) : (x + y);
}

int dec(int x, int y) {
    return (x - y) < 0 ? (x - y + mod) : (x - y);
}

int power(int x, int y) {
    int ans = 1;
    for(;y;y >>= 1) {
        if(y & 1)
            ans = 1ll * ans * x % mod;
        x = 1ll * x * x % mod;
    }
    return ans;
}

int fac[maxn], inv[maxn], n;

int C(int n, int m) {
    if(n < 0||m < 0||n < m)
        return 0;
    return 1ll * fac[n] * inv[n - m] % mod * inv[m] % mod;
}

void chkmax(int& x, int y) {
    if(y > x)
        x = y;
    return ;
}

struct DPAM {
    int f[3][3];
    DPAM() {
        memset(f, -1, sizeof(f));
    }
    void clear() {
        memset(f, -1, sizeof(f));
        return ;
    }
    DPAM operator + (const int& x) const {
        DPAM res;
        for(int i = 0;i <= 2;++ i)
            for(int j = 0;j <= 2;++ j)
                if(~ f[i][j])
                    for(int k = 0;k <= 2;++ k)
                        if(x >= i + j + k)
                            chkmax(res.f[j][k], std::min(4, f[i][j] + i + (x - i - j - k) / 3));
        return res;
    }
};

void Fmax(DPAM& A, DPAM B) {
    for(int i = 0;i <= 2;++ i)
        for(int j = 0;j <= 2;++ j)
            chkmax(A.f[i][j], B.f[i][j]);
    return ;
}

struct node {
    int cnt;
    DPAM dp[2];
    node() {
        cnt = 0;
        dp[0].clear();
        dp[1].clear();
    }
    void clear() {
        return cnt = 0, dp[0].clear(), dp[1].clear(), void();
    }
    bool operator < (const node& p) const {
        for(int k = 0;k < 2;++ k)
            for(int i = 0;i <= 2;++ i)
                for(int j = 0;j <= 2;++ j)
                    if(dp[k].f[i][j] ^ p.dp[k].f[i][j])
                        return dp[k].f[i][j] < p.dp[k].f[i][j];
        return cnt < p.cnt;
    }
};

node win() {
    node res;
    res.cnt = 114514;
    return res;
}

bool check(node& x) {
    if(x.cnt >= 7)
        return true;
    for(int i = 0;i <= 2;++ i)
        for(int j = 0;j <= 2;++ j)
            if(x.dp[1].f[i][j] >= 4)
                return true;
    return false;
}

node operator + (const node& A, const int& x) {
    if(A.cnt == 114514)
        return A;
    node res;
    Fmax(res.dp[0], A.dp[0] + x);
    Fmax(res.dp[1], A.dp[1] + x);
    res.cnt = A.cnt + (x >= 2);
    if(x >= 2)
        Fmax(res.dp[1], A.dp[0] + (x - 2));
    if(check(res))
        return win();
    return res;
}

int tot, dp[2][maxn][maxm], tr[maxm][5], winid, ans, now, cnt[maxn];
std::map<node, int> mp;

void bfs() {
    std::queue<node> q;
    node res;
    res.dp[0].f[0][0] = 0;
    q.emplace(res);
    mp[res] = ++ tot;
    while(!q.empty()) {
        node A = q.front();
        q.pop();
        int id = mp[A];
        for(int i = 0;i <= 4;++ i) {
            node B = A + i;
            if(!mp.count(B)) {
                tr[id][i] = mp[B] = ++ tot;
                q.emplace(B);
                if(B.cnt == 114514)
                    winid = tot;
            }
            else
                tr[id][i] = mp[B];
        }
    }
    return ;
}

int main() {
    scanf("%d", &n);
    fac[0] = 1;
    for(int i = 1;i <= 4 * n;++ i)
        fac[i] = 1ll * fac[i - 1] * i % mod;
    inv[4 * n] = power(fac[4 * n], mod - 2);
    for(int i = 4 * n - 1;i >= 0;-- i)
        inv[i] = 1ll * inv[i + 1] * (i + 1) % mod;
    bfs();
    for(int i = 1, x, y;i <= 13;++ i)
        scanf("%d %d", &x, &y), ++ cnt[x];
    dp[now = false][0][1] = 1;
    for(int i = 1;i <= n;++ i) {
        now ^= true;
        memset(dp[now], 0, sizeof(dp[now]));
        for(int j = 1;j <= tot;++ j)
            for(int k = 0;k <= 4 * (i - 1);++ k)
                for(int t = 0;t <= 4 - cnt[i];++ t)
                    add(dp[now][k + t][tr[j][t + cnt[i]]], 1ll * dp[now ^ true][k][j] * C(4 - cnt[i], t) % mod);
    }
    for(int i = 0;i <= 4 * n - 13;++ i)
        for(int j = 1;j <= tot;++ j)
            if(j ^ winid)
                add(ans, 1ll * power(C(4 * n - 13, i), mod - 2) * dp[now][i][j] % mod);
    printf("%d\n", ans);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值