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\),时间复杂度是 \(O(2^nnm)\) 的.
现在要求剩下的方案数了,我们相当于要用 \(m-|S|\) 个数表示剩下的怪兽,给剩下的长度为 \(m-w(S)\) 的攻击序列染色,第 \(i\) 种颜色有一个上界 \(a_i-1\),染色表示的是这次攻击打给了哪个怪兽,就染对应的颜色.
这其实是一个背包,具体地,我们可以这样计算,设 \(g(S, i)\) 表示有 \(i\) 次攻击在 \(S\) 中并且 \(S\) 中一个没死的方案数,注意这里概率我们已经求出了,剩下的是计算没有确定的位置有多少方案,所以设的是方案数. 而 \(g\) 的转移比较显然,我们可以钦定 \(S\) 中的某个元素来转移,不妨钦定最小的是 \(v\),那么就要枚举有 \(j\) 次攻击打到了 \(v\) 身上进行转移,式子如下:
注意 \(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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端