Gym102798 CCPC2020威海 E 加强版 题解

原题link
昨天模拟赛考了这个的加强版,很神秘,来写个题解.
数据范围就把 \(m\)\(a_i\) 的上界改成 \(200\),并且答案对 \(998244353\) 取模,其他不变.

基本思路:枚举 \(S\),求出 \(p(S)\) 表示集合 \(S\) 中的怪兽被打死的概率,答案就是 \(\sum_{S} |S|p(S)\).

而这个集合 \(S\) 满足一些性质,具体来说对于 \(x \in S\), 它恰好被攻击了 \(a_x\) 次,而对于 \(x \notin S\), 它至多被攻击了 \(a_x-1\) 次. 这启发我们把 \(p\) 拆成两个部分,第一部分计算 \(S\) 中恰好被打死的概率,第二部分计算 \(S\) 以外的没被打死的方案数,注意在第一部分中计算的是所有等概率情形,所以第二部分要计算方案数.

具体的,来说,考虑如下的 DP:

我们考虑一个长度为 \(m\) 的攻击序列 \(j_1, j_2, \cdots, j_m\) 表示每次被攻击的怪兽的编号,那么一个显然的 dp 是说,设 \(f(s, i)\) 表示考虑前 \(i\) 次攻击(即 \(j_1, j_2, \cdots, j_i\))已经打死了 \(S\) 集合中的怪兽,概率是多少.

考虑如何转移这个 \(f\).

为了简便,我们记 \(w(S)=\sum_{x \in S}a_x\).

考虑第 \(i\) 次攻击,是正好杀死了一个怪兽还是打在了别的地方。

第一种情况是说我们不确定位置(相当于等概率随机),那么直接乘上概率转移即可, 即 \(f(S, i - 1) \frac{1}{n - |S|}\).

第二种情况是我们枚举最后一次杀死了集合 \(S\) 中的一个元素 \(v\), 则是从 \(f(S \backslash \{v\}, i - 1)\) 转移过来的. 首先要乘上对应的概率系数 \(\frac{1}{n-|S \backslash \{v\}|}=\frac{1}{n-|S|+1}\),表示在这些怪兽中正好打到了这一个,但是还有一个限制是说在这之前,\(v\) 这个怪兽应该只剩一滴血了,那么我们在前面的攻击序列中需要找出 \(a_v-1\) 个位置来打 \(v\) 使得 \(v\) 只有一滴血,而前面的 \(i - 1\) 长度的攻击序列中,有一部分是必须用来打 \(S\backslash\{v\}\),剩下的才是自由的,所以这个系数就是 \(i - 1 - w(S \backslash \{v\}) \choose a_v-1\) .

对于第二种情况的理解:一开始看到可能觉得奇怪,为什么限制 \(v\) 怪兽只剩一滴血反而还要乘上一个系数使概率变大了,其实这个是比较显然的,我们在 \(f(S\backslash\{v\}, i - 1)\) 中只考虑了 \(S\backslash\{v\}\) 被打死之后的所有等概率情形的概率,而在限定了 \(v\) 这个怪兽只有一滴血后,我们的情形变多了,因为在所有的等概率情形中,有 \(i - 1 - w(S \backslash \{v\}) \choose a_v-1\) 种都满足 \(v\) 这个怪兽只剩一滴血的限制.

于是可以列出转移式子如下:

\[f(S, i) = \frac{1}{n - |S|}f(S, i - 1) + \frac{1}{n-|S|+1}\sum_{v \in S} f(S\backslash\{v\}, i - 1)\binom{i - 1 - w(S\backslash\{v\})}{a_v-1} \]

直接暴力转移 \(f\),时间复杂度是 \(O(2^nnm)\) 的.

现在要求剩下的方案数了,我们相当于要用 \(m-|S|\) 个数表示剩下的怪兽,给剩下的长度为 \(m-w(S)\) 的攻击序列染色,第 \(i\) 种颜色有一个上界 \(a_i-1\),染色表示的是这次攻击打给了哪个怪兽,就染对应的颜色.

这其实是一个背包,具体地,我们可以这样计算,设 \(g(S, i)\) 表示有 \(i\) 次攻击在 \(S\) 中并且 \(S\) 中一个没死的方案数,注意这里概率我们已经求出了,剩下的是计算没有确定的位置有多少方案,所以设的是方案数. 而 \(g\) 的转移比较显然,我们可以钦定 \(S\) 中的某个元素来转移,不妨钦定最小的是 \(v\),那么就要枚举有 \(j\) 次攻击打到了 \(v\) 身上进行转移,式子如下:

\[g(S, i)=\sum_{j=0}^{\min(a_v-1,i)}{i \choose j}g(S\backslash\{v\},i-j) \]

注意 \(min(a_v - 1, i)\) 是说打在 \(v\) 上的攻击不能超过 \(a_v-1\),否则会杀死 \(v\),并且不能超过 \(i\),因为总共就 \(i\) 次攻击.

最终用 \(f\)\(g\) 配合,求出答案:\(\sum_S |S| f(S, m)g(\overline S, m-w(S))\).

到这里其实已经可以通过原题了,复杂度是 \(O(2^n m^2)\) 的,但是过不了 \(200\),我们考虑优化这个 \(g\) 的转移.

可以使用 meet in the middle 的技巧,我们先暴力计算 \(S\) 的前 \(n/2\) 位和后 \(n/2\) 位,称为 \(g_1,g_2\),这样的复杂度是 \(O(2^{n/2}m^2)\) 的.(前表示高位,后表示低位)

考虑设 \(S\) 的前 \(n/2\) 位是 \(x\),后 \(n/2\) 位是 \(y\),则 \(g(S,i)=\sum_{j+k=i}g_1(x,j)g_2(y,k)\binom{i}{j}\).

换句话说,我们可以以 \(O(m)\) 的代价合并对于某个特定 \(g(S, i)\).

注意到我们只关心 \(g(\overline S, m-w(S))\). 可以用 \(O(2^n m)\) 的复杂度处理出这些 \(g\) 的值,然后统计答案即可.

最终复杂度是 \(O(2^n nm)\),可以通过.

总结:

这个题核心在于把 \(p\) 拆成 \(f*g\)\(f\) 是满足约束情况下的概率\(g\) 则是剩下部分的方案数,这种思想在 \(f\) 的转移中的第二种情形也有所体现,建议读者仔细体会.

但是我常数大的一批,不开 O2 的话用 mim 和不用 mim 一个分,开 O2 才过,大概率是取模板子不太行,建议还是少偷懒.
优化了一下,现在不开 O2 也能过了
代码

#include <bits/stdc++.h>
using namespace std;

#define ll long long

const int maxn = 20, maxs = (1 << 15) + 5, maxm = 205;
const ll mod = 998244353;
int n, m;
int a[maxn];
int w[maxs], cnt[maxs];
int lg[maxs];
ll f[maxs][maxm], g[maxs];
ll g1[maxs][maxm], g2[maxs][maxm];
ll inv[maxm];
ll c[maxm][maxm];

ll cal(int s, int i) { // mim
    int A = n / 2, B = n - A;
    int x = s >> B;
    int y = ((1 << B) - 1) & s;
    ll ans = 0;
    for (int j = 0; j <= i; j++) {
        ans += g1[x][j] * g2[y][i - j] % mod * c[i][j] % mod;
        ans %= mod;
    }
    return ans;
}

int main() {
    scanf("%d%d", &n, &m);
    inv[1] = 1;
    for (int i = 2; i <= n + 1; i++)
        inv[i] = ((mod - mod / i) * inv[mod % i]) % mod;
    for (int s = 2; s < 1 << n; s++)
        lg[s] = lg[s >> 1] + 1;
    c[0][0] = 1;
    for (int i = 1; i <= m; i++) {
        c[i][0] = 1;
        for (int j = 1; j <= i; j++)
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
    }

    for (int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    for (int s = 1; s < 1 << n; s++)
        w[s] = w[s & (s - 1)] + a[lg[s & -s]],
        cnt[s] = cnt[s & (s - 1)] + 1;
    // calc f
    f[0][0] = 1;
    for (int i = 1; i <= m; i++)
        for (int s = 0; s < 1 << n; s++) {
            if (n != cnt[s])
                f[s][i] = f[s][i - 1] * inv[n - cnt[s]] % mod;
            ll r = 0;
            for (int j = 0; j < n; j++)
                if (s & (1 << j)) {
                    if (i - 1 - w[s ^ (1 << j)] < 0) continue;
                    r += f[s ^ (1 << j)][i - 1] * c[i - 1 - w[s ^ (1 << j)]][a[j] - 1] % mod;
                    r %= mod;
                }
            f[s][i] += r * inv[n - cnt[s] + 1] % mod;
            r %= mod;
        }
    int A = n / 2, B = n - A;
    for (int s = 0; s < 1 << A; s++)
        g1[s][0] = 1;
    for (int s = 0; s < 1 << B; s++)
        g2[s][0] = 1;
    for (int i = 1; i <= m; i++) {
        // calc g1
        for (int s = 1; s < 1 << A; s++) {
            int v = lg[s & -s], t = min(i, a[v + B] - 1);
            for (int j = 0; j <= t; j++) {
                g1[s][i] += c[i][j] * g1[s ^ (1 << v)][i - j] % mod;
                g1[s][i] %= mod;
            }
        }
        // calc g2
        for (int s = 1; s < 1 << B; s++) {
            int v = lg[s & -s], t = min(i, a[v] - 1);
            for (int j = 0; j <= t; j++) {
                g2[s][i] += c[i][j] * g2[s ^ (1 << v)][i - j] % mod;
                g2[s][i] %= mod;
            }
        }
    }
    ll ans = 0;
    for (int s = 0; s < 1 << n; s++)
        if (m >= w[s]) {
            ans += cnt[s] * f[s][m] % mod * cal(((1 << n) - 1) ^ s, m - w[s]) % mod;
            ans %= mod;
        } 
    cout << ans << endl;
    return 0;
}
posted @ 2022-08-15 11:15  71rats  阅读(95)  评论(0编辑  收藏  举报