九条可怜最近在玩一款卡牌游戏。在每一局游戏中,可怜都要使用抽到的卡牌来消灭一些敌人。每一名敌人都有一个初始血量,而当血量降低到 0 及以下的时候,这名敌人就会立即被消灭并从场上消失。

现在,可怜面前有 n 个敌人,其中第 i 名敌人的血量是 ai​,而可怜手上只有如下两张手牌:

  1. 如果场上还有敌人,等概率随机选中一个敌人并对它造成一点伤害(即血量减 1),重复 K 次。

  2. 对所有敌人造成一点伤害,重复该效果直到没有新的敌人被消灭。

下面是这两张手牌效果的一些示例:

  1. 假设存在两名敌人,他们的血量分别是 1,2 且 K=2。那么在可怜打出第一张手牌后,可能会发生如下情况:
  • 第一轮中,两名敌人各有 0.5 的概率被选中。假设第一名敌人被选中,那么它会被造成一点伤害。这时它的血量变成了 0,因此它被消灭并消失了。
  • 第二轮中,因为场上只剩下了第二名敌人,所以它一定会被选中并被造成一点伤害。这时它剩下的血量为 1。
  1. 同样假设存在两名敌人且血量分别为 1,2。那么在可怜打出第二张手牌后,会发生如下情况:
  • 第一轮中,所有敌人被造成了一点伤害。这时第一名敌人被消灭了,因此卡牌效果会被重复一遍。
  • 第二轮中,所有敌人(此时只剩下第二名敌人了)被造成了一点伤害。这时第二名敌人也被消灭了,因此卡牌效果会被再重复一遍。
  • 第三轮中,所有敌人(此时没有敌人剩下了)被造成了一点伤害。因为没有新的敌人被消灭了,所以卡牌效果结束。
  1. 如果面对的是四名血量分别为 1,2,2,4 的敌人,那么在可怜打出第二张手牌后,只有第四名敌人还会存活,且它的剩余血量为 1。

现在,可怜先打出了第一张手牌,再打出了第二张手牌。她发现,在第一张手牌效果结束后,没有任何一名敌人被消灭,但是在第二张手牌的效果结束后,所有敌人都被消灭了。

可怜想让你计算一下这种情况发生的概率是多少。

输入格式:

第一行输入两个整数 n,K(1≤n,K≤50),分别表示敌人的数量以及第一张卡牌效果的发动次数。

第二行输入 n 个由空格隔开的整数 ai​(1≤ai​≤50),表示每个敌人的初始血量。

输出格式:

在一行中输出一个整数,表示发生概率对 998244353 取模后的结果。

具体来说,如果概率的最简分数表示为 a/b(a≥0,b≥1,gcd(a,b)=1),那么你需要输出

a×b998244351mod998244353。

输入样例 1:

3 2
2 3 3

输出样例 1:

665496236

样例解释 1:

在第一张手牌的效果结束后,三名敌人的剩余血量只可能在如下几种中:[1,3,2], [1,2,3], [2,1,3] 和 [2,3,1]。前两种发生的概率是 2/9,后两种发生的概率是 1/9。因此答案为 2/3,输出 2×3998244351mod998244353=665496236。

输入样例 2:

3 3
2 3 3

输出样例 2:

776412275

样例解释 2:

在第一张手牌的效果结束后,三名敌人的剩余血量只可能在如下几种中:[1,2,2]、[2,1,2] 和 [2,2,1]。第一种发生的概率是 2/9,后两种发生的概率是 1/9。因此答案为 4/9,输出 4×9998244351mod998244353=776412275。

输入样例 3:

5 3
1 4 4 2 5

输出样例 3:

367353922

输入样例 4:

12 12
1 2 3 4 5 6 7 8 9 10 11 12

输出样例 4:

452061016

代码长度限制

16 KB

时间限制

2000 ms

内存限制

256 MB

栈限制

8192 KB

import java.util.*;
import java.io.*;

/**
 * 题目:L3-033 教科书般的亵渎
 * 思路:
 * 1. 状态压缩 DP:利用 long 类型的位掩码 (mask) 表示 1-50 血量怪物的存在情况。
 * 2. 滚动优化:按怪物序号 i 进行阶段划分,使用两个 Map 数组 (dp 和 nxt) 滚动更新。
 * 3. 性能核心:
 * - 剪枝:预判当前 mask 在剩余 K 次调整下,是否能补齐后续所有怪物的血量断层。
 */
public class Main {
    static final int MOD = 998244353;
    static int n, K;
    static int[] h;   // 怪物原始血量
    static int[][] C; // 预处理组合数

    // 线性探测高效哈希表
    static class Map {
        long[] ks; // keys: mask
        int[] vs;  // vals: ways
        int sz, cap, mask;

        Map(int c) {
            cap = c; mask = c - 1;
            ks = new long[c]; vs = new int[c];
            Arrays.fill(vs, -1);
        }

        void clear() {
            if (sz == 0) return;
            Arrays.fill(vs, -1);
            sz = 0;
        }

        // 扰动函数,将相似的 mask 散布到数组各处,避免线性探测聚簇
        private int hash(long k) {
            long h = k ^ (k >>> 33);
            h *= 0xff51afd7ed558ccdL;
            h ^= (h >>> 33);
            h *= 0xc4ceb9fe1a85ec53L;
            h ^= (h >>> 33);
            return (int) h & mask;
        }

        void put(long k, int v) {
            int i = hash(k);
            while (vs[i] != -1) {
                if (ks[i] == k) {
                    vs[i] = (vs[i] + v) % MOD;
                    return;
                }
                i = (i + 1) & mask;
            }
            ks[i] = k; vs[i] = v;
            sz++;
        }
    }

    /**
     * 剪枝:模拟“亵渎”触发过程
     * cur: 当前血量掩码, rem: 剩余调整次数, idx: 下一个处理的怪物序号
     */
    static boolean check(long cur, int rem, int idx) {
        int cost = 0;
        for (int i = idx; i <= n; i++) {
            // 找到当前掩码中最低位的 0 (即第一个能中断亵渎的空缺)
            long gap = (cur + 1) & (~cur);
            if (gap > (1L << 50)) break; // 已经连通到最大血量上限
            
            // 如果当前怪物血量已经落在已有的 mask 中,它会被亵渎连带消灭,跳过
            if ((1L << (h[i] - 1)) < gap) continue;
            
            // 否则,必须消耗调整次数将当前怪物 h[i] 降低到空缺位置 gap 处
            int target = Long.numberOfTrailingZeros(gap) + 1;
            cost += (h[i] - target);
            if (cost > rem) return false; // 剩余次数不足以填补此断层
            cur |= gap; // 模拟填补
        }
        return true;
    }

    public static void main(String[] args) throws IOException {
        FastReader fr = new FastReader(System.in);
        n = fr.nextInt(); K = fr.nextInt();
        h = new int[n + 1];
        for (int i = 1; i <= n; i++) h[i] = fr.nextInt();
        Arrays.sort(h, 1, n + 1); // 必须排序,确保按血量从小到大处理

        // 预处理组合数 C(n, k)
        C = new int[K + 1][K + 1];
        for (int i = 0; i <= K; i++) {
            C[i][0] = C[i][i] = 1;
            for (int j = 1; j < i; j++) C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD;
        }

        // 分层滚动 DP:dp[r] 表示剩余 r 次调整机会时的状态集合
        Map[] dp = new Map[K + 1];
        Map[] nxt = new Map[K + 1];
        for (int j = 0; j <= K; j++) {
            dp[j] = new Map(1 << 14);
            nxt[j] = new Map(1 << 14);
        }

        dp[K].put(0L, 1); // 初始状态:掩码为空,剩余 K 次机会
        int total = 0;

        for (int i = 1; i <= n; i++) {
            int curH = h[i];
            for (int j = 0; j <= K; j++) nxt[j].clear();

            for (int r = 0; r <= K; r++) {
                Map m = dp[r];
                if (m.sz == 0) continue;

                for (int j = 0; j < m.cap; j++) {
                    if (m.vs[j] == -1) continue;
                    
                    long curM = m.ks[j];
                    int ways = m.vs[j];

                    // 枚举对当前怪物消耗的次数 cost,使其血量变为 curH - cost
                    for (int cost = 0; cost <= r; cost++) {
                        int val = curH - cost;
                        if (val < 1) break;

                        long nxtM = curM | (1L << (val - 1));
                        int nxtR = r - cost;
                        int add = (int) (1L * ways * C[r][cost] % MOD);

                        if (i == n) {
                            // 最终状态检查:必须用完所有次数,且 mask 必须是全连通状态 (11...1)
                            if (nxtR == 0 && (nxtM & (nxtM + 1)) == 0) total = (total + add) % MOD;
                        } else {
                            // 剪枝判断,通过则加入下一层
                            if (!check(nxtM, nxtR, i + 1)) continue;
                            if (nxt[nxtR].sz > nxt[nxtR].cap * 0.65) rehash(nxt, nxtR);
                            nxt[nxtR].put(nxtM, add);
                        }
                    }
                }
            }
            // 滚动数组交换
            Map[] t = dp; dp = nxt; nxt = t;
        }

        // 最终结果 = 合法方案数 * (n^K 的逆元)
        long inv = pow(pow(n, K), MOD - 2);
        System.out.println(1L * total * inv % MOD);
    }

    // 哈希表自动扩容
    static void rehash(Map[] maps, int i) {
        Map old = maps[i];
        Map nm = new Map(old.cap << 1);
        for (int j = 0; j < old.cap; j++) {
            if (old.vs[j] != -1) nm.put(old.ks[j], old.vs[j]);
        }
        maps[i] = nm;
    }

    static long pow(long b, long e) {
        long r = 1; b %= MOD;
        while (e > 0) {
            if ((e & 1) == 1) r = r * b % MOD;
            b = b * b % MOD; e >>= 1;
        }
        return r;
    }

    // 快读
    static class FastReader {
        private InputStream is;
        private byte[] buf = new byte[1 << 16];
        private int p, l;
        FastReader(InputStream s) { is = s; }
        private int read() throws IOException {
            if (p == l) { p = 0; l = is.read(buf); if (l <= 0) return -1; }
            return buf[p++] & 0xff;
        }
        int nextInt() throws IOException {
            int c = read(), r = 0;
            while (c >= 0 && c <= 32) c = read();
            while (c > 32) { r = r * 10 + (c - '0'); c = read(); }
            return r;
        }
    }
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐