P4564 [CTSC2018]假面
没想到能在短短的3天碰见两次这种长得像背包的 dp 的撤销问题,故有此文章。
题意
有 \(n\) 个人,每个人有 \(m[i]\) 的初始血量。
有两种操作:第一种以概率 \(p\) 对指定人攻击,有 \(p\) 的概率生命值 \(-1\),\(1 - p\) 的概率生命值不变。第二种给定一群目标,对这些人释放一次只能命中一个人的攻击,以活人的人头数等概率攻击,问每个人被攻击的概率是多少。
最后一个问题,问所有操作结束后,每个人血量的期望。
Solution
首先可以发现,第二种操作不会影响到第一种操作。
接下来逐步分析。
操作一:
我们令
q[i][j]
表示第 \(i\) 个人的血量为 \(j\) 的概率。可以发现,每次攻击都会向下转移。
q[i][j] *= (1-p)
q[i][j] += q[i][j+1] * p
分别对应当前血量在攻击中存活,或者
j+1
的血量被攻击。于是我们处理完了操作一。
操作二:
发现这个问题不好求的地方就在于,我不确定死了几个,活了几个。
那么考虑搞一个这个东西出来,令
g[i][j]
表示除了第 \(i\) 个人,活了 \(j\) 个的概率。那么第 \(i\) 个人的答案就是 \(h[i] = alive(i)\sum_{j=0}^{k-1}\cfrac{g[i][j]}{j+1}\)
,其中alive(i)
表示i
存活概率发现这个东西依然不好求,那我们再来一个 dp。
考虑令
f[i][j]
表示前 \(i\) 个人,活了 \(j\) 个的概率,那么显然对于一个人 \(x\) ,我只要把他扔到最后面就好了,那么它的 \(g[x][j]\) 就是 \(f[k-1][j]\)。对于 \(f\) ,可以写出来转移
f[i][j] = f[i-1][j] * dead(i) + f[i-1][j-1] * alive(i)
这个东西可以用背包滚动掉第一维,也就是倒过来枚举。
f[j] = f[j] * dead(i) + f[j-1] * alive(i)
于是你就有了 \(O(n^3)\) 的 \(70\) 哩!
继续优化。
考虑我们现在是把每个 \(x\) 扔到最后面,求出来了前面的,但是背包的过程可以不分先后,那么我们可不可以先求出来整体的,然后每次扣掉第 \(i\) 个呢?
当然可以哩!
我愿称这一类套路为背包滴撤销。
首先我们可以给一些公式上的推导。
\[f[i][j] = f[i-1][j] \times dead(i) + f[i-1][j-1] \times alive(i) \]\[f[i-1][j] = \cfrac{f[i][j] - f[i-1][j-1]\times alive(i)}{dead(i)} \]注意在 \(dead(i) = 0\) 的时候没有意义,但就 \(f[i-1][j] = f[i][j+1]\),所以不用怕。
是不是看起来非常合理,感觉我撤销了,但我其实不太懂这种推导,我比较喜欢暴力用手模拟一下过程,以下默认滚动掉第一维。
- \(i = 0:\) \(f[0], f[1], f[2]\)
- \(i = 1:\) \(f[0]d[1],f[1]d[1] + f[0]a[1]\) ,\(f[2]d[1] + f[1]a[1]\)
- \(i = 2:\) \(f[0]d[1]d[2]\),\((f[1]d[1] + f[0]a[1])d[2] + f[0]d[1]a[2]\),\((f[2]d[1] + f[1]a[1])d[2] + (f[1]d[1] + f[0]a[1])d[1]\)
那么我们假设只有这 \(2\) 个东西,现在求出了包含所有人的 \(dp\) ,考虑如何扣掉和 第 \(1\) 个人有关的转移,也就是去除所有 \(d[1],a[1]\)。
回到了前面推的式子了,这回我们顺推,考虑得到所有的 \(g[j]\),注意也是滚掉第一维的,这里代表考虑除了第 \(1\) 个人有 \(j\) 个人存活的概率。
开始模拟!
首先对于 \(g[0]\) 显然就是 \(\cfrac{f[0]}{d[1]}\),此时我们得到的 \(g[0] = f[0]d[2]\)。
注意,接下来 \(g\) 数组实际上是变成了 \(f_{i-1}\) ,然后我们继续用 \(f,g\) 得到新的 \(g\) 。
于是你观察 \(f[1]\) 的式子,你发现 \(\cfrac{f[1] -g[0]a[1] }{d[1]}\) 居然写出来正好消掉了所有滴 \(a[1],d[1]\) ,好神奇啊!!
再来看 \(f[2]\) ,我们发现 \(\cfrac{f[2] - g[1]a[1]}{d[1]}\) 居然依旧完美消去了 \(a[1],d[1]\) ,至此,相信大家能理解这种撤销过程的正确性了,非常神奇滴。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define Rep(i,a,b) for(int i=(a);i<(b);++i)
#define rrep(i,a,b) for(int i=(a);i>=(b);--i)
using namespace std;
template <typename T>
inline void read(T &x){
x=0;char ch=getchar();bool f=0;
while(ch<'0'||ch>'9'){if(ch=='-')f=1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
if(f)x=-x;
}
template <typename T,typename ...Args>
inline void read(T &tmp,Args &...tmps){read(tmp);read(tmps...);}
const int mod = 998244353;
const int N = 205;
using ll = long long;
ll f[N],g[N],h[N],t[N],q[N][N],Inv[N];
int n,k,a[N];
inline int ksm(int x,int y){
ll res = 1;
while(y){
if(y & 1)res = res * x % mod;
x = 1ll * x * x % mod;
y >>= 1;
}
return res;
}
inline int inv(int x){
return ksm(x,mod - 2);
}
inline void op0(int i,int p){
int _p = (1 - p + mod) % mod;
rep(j,0,a[i]){
j > 0 ? q[i][j] = q[i][j] * _p % mod : 0;
j < a[i] ? q[i][j] = (q[i][j] + q[i][j + 1] * p) % mod : 0;
}
}
inline void op1(){
memset(f,0,sizeof(f));
memset(h,0,sizeof(h));
memset(g,0,sizeof(g));
f[0] = 1;
rep(i,1,k)
rrep(j,i,0)
f[j] = (f[j] * q[t[i]][0] + (j ? f[j - 1] * (1 - q[t[i]][0]) : 0)) % mod;
rep(i,1,k){
if(!q[t[i]][0])Rep(j,0,k)h[i] += f[j + 1] * Inv[j + 1] % mod;
else{
int tmp = inv(q[t[i]][0]);
Rep(j,0,k){
g[j] = (f[j] - (j ? g[j - 1] * (1 - q[t[i]][0]) : 0)) % mod * tmp % mod;
h[i] += g[j] * Inv[j + 1] % mod;
}
}
h[i] %= mod;
h[i] = h[i] * (1 - q[t[i]][0]) % mod;
if(h[i] < 0)h[i] += mod;
}
rep(i,1,k)printf("%lld ",h[i]);
puts("");
}
signed main(){
read(n);
rep(i,1,n)read(a[i]),q[i][a[i]] = 1,Inv[i] = inv(i);
int Q;
read(Q);
while(Q--){
int op;
read(op);
if(!op){
int id,u,v;
read(id,u,v);
u = 1ll * u * inv(v) % mod;
op0(id,u);
}
else{
read(k);
rep(i,1,k)read(t[i]);
op1();
}
}
rep(i,1,n){
ll res = 0;
rep(j,0,a[i])res += j * q[i][j] % mod;
printf("%lld ",res % mod);
}
}