九条可怜最近在玩一款卡牌游戏。在每一局游戏中,可怜都要使用抽到的卡牌来消灭一些敌人。每一名敌人都有一个初始血量,而当血量降低到 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 教科书般的亵渎
 */
public class Main {
    static final int MOD = 998244353;
    static int n, K;
    static int[] h;
    static int[][] C;

    static class Map {
        long[] ks;
        int[] vs;
        int[] pos; // 密集存储已占用索引,使遍历达到 O(sz)
        int sz, cap, mask;

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

        void clear() {
            // 仅清理有数据的部分,加速滚动过程
            for (int i = 0; i < sz; i++) {
                vs[pos[i]] = -1;
            }
            sz = 0;
        }

        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] += v; 
                    if (vs[i] >= MOD) vs[i] -= MOD; // 减法代替取模
                    return;
                }
                i = (i + 1) & mask;
            }
            ks[i] = k; vs[i] = v;
            pos[sz++] = i; // 记录实际存有数据的索引位置
        }
    }

    // 剪枝函数
    static boolean check(long cur, int rem, int idx) {
        int cost = 0;
        for (int i = idx; i <= n; i++) {
            int zeroPos = Long.numberOfTrailingZeros(~cur); // 优化:直接找最右侧的 0
            if (zeroPos > 50) break; 
            
            // h[i] 的原始血量如果不大于第一个缺失血量(zeroPos+1),意味着它能顺带被消灭
            if (h[i] - 1 < zeroPos) continue; 
            
            cost += (h[i] - (zeroPos + 1));
            if (cost > rem) return false; 
            cur |= (1L << zeroPos); 
        }
        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 = 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];
                if (C[i][j] >= MOD) C[i][j] -= MOD; // 加法优化
            }
        }

        // 空间换时间
        Map[] dp = new Map[K + 1];
        Map[] nxt = new Map[K + 1];
        for (int j = 0; j <= K; j++) {
            dp[j] = new Map(1 << 15);
            nxt[j] = new Map(1 << 15);
        }

        dp[K].put(0L, 1);
        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 idx = 0; idx < m.sz; idx++) {
                    int j = m.pos[idx];
                    long curM = m.ks[j];
                    int ways = m.vs[j];

                    // 优化:处理最后一只目标的特判逻辑,剥离出内层循环
                    if (i == n) {
                        int val = curH - r; // r 必须一次性用完,cost == r
                        if (val >= 1) {
                            long nxtM = curM | (1L << (val - 1));
                            // 必须是全连通状态
                            if ((nxtM & (nxtM + 1)) == 0) {
                                // C[r][r] == 1,所以无需乘组合数,直接加 ways
                                total += ways;
                                if (total >= MOD) total -= MOD;
                            }
                        }
                        continue;
                    }

                    // 对于普通目标的转移
                    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;

                        if (!check(nxtM, nxtR, i + 1)) continue;

                        int add = (int) (1L * ways * C[r][cost] % MOD);
                        
                        if (nxt[nxtR].sz > nxt[nxtR].cap * 0.65) rehash(nxt, nxtR);
                        nxt[nxtR].put(nxtM, add);
                    }
                }
            }
            Map[] t = dp; dp = nxt; nxt = t;
        }

        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);
        // rehash 也只需遍历有效项
        for (int j = 0; j < old.sz; j++) {
            int idx = old.pos[j];
            nm.put(old.ks[idx], old.vs[idx]);
        }
        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社区

更多推荐