[LOJ#3119][Luogu5405][CTS2019]氪金手游(DP+容斥)
先考虑外向树的做法,显然一个点在其子树内第一个出现的概率等于它的权值除以它子树的权值和。于是f[i][j]表示i的子树的权值和为j时,i子树内所有数的相互顺序都满足条件的概率,转移直接做一个背包卷积即可。
现在考虑反向边,通过容斥变成“至少有i条边不满足条件”的满足题目条件的概率,这样一来那些反向边会有一部分被变为正向边,另一部分被删除。如果枚举哪些边被反向的话可以做到$O(2^nn^2)$。但事实上我们并不关心具体是哪些边被反向了,而只关心有多少边被反向了。于是自然有一个方程f[i][j][k]表示i的子树的权值和为j,有k条边被反向的满足条件的概率。再注意到最后一维也是可以不要的,因为我们可以在DP过程中就计入容斥系数。感性理解,当一条从儿子连过来的边被反向时,它在最终结果中的系数一定与当前点的子树的系数相反,而当一条边被删除时,它的子树与当前点的子树之间独立,且最终系数与当前点的系数相同。复杂度$O(n^2)$,具体转移见代码。
1 #include<cstdio> 2 #include<algorithm> 3 #define rep(i,l,r) for (int i=(l); i<=(r); i++) 4 #define For(i,x) for (int i=h[x],k; i; i=nxt[i]) 5 typedef long long ll; 6 using namespace std; 7 8 const int N=3010,mod=998244353,inf=1e9; 9 int n,rt,ans,cnt,u,v,inv[N],a[N],b[N],c[N],s[N],f[N][N],sz[N],g[N],h[N],to[N],val[N],nxt[N]; 10 11 void add(int u,int v,int w){ to[++cnt]=v; val[cnt]=w; nxt[cnt]=h[u]; h[u]=cnt; } 12 void inc(int &x,int y){ x=(x+y>=mod) ? x+y-mod : x+y; } 13 void dec(int &x,int y){ x=(x-y<0) ? x-y+mod : x-y; } 14 15 int ksm(int a,int b){ 16 int res=1; 17 for (; b; a=1ll*a*a%mod,b>>=1) 18 if (b & 1) res=1ll*res*a%mod; 19 return res; 20 } 21 22 void dfs(int x,int fa){ 23 sz[x]=1; 24 f[x][1]=1ll*a[x]*s[x]%mod; 25 f[x][2]=2ll*b[x]*s[x]%mod; 26 f[x][3]=3ll*c[x]*s[x]%mod; 27 For(z,x) if ((k=to[z])!=fa){ 28 dfs(k,x); 29 rep(i,1,sz[x]*3) rep(j,1,sz[k]*3){ 30 int t=1ll*f[x][i]*f[k][j]%mod; 31 if (val[z]) dec(g[i+j],t),inc(g[i],t); else inc(g[i+j],t); 32 } 33 sz[x]+=sz[k]; 34 rep(i,1,sz[x]*3) f[x][i]=g[i],g[i]=0; 35 } 36 rep(i,1,sz[x]*3) f[x][i]=1ll*f[x][i]*inv[i]%mod; 37 } 38 39 int main(){ 40 freopen("fgo.in","r",stdin); 41 freopen("fgo.out","w",stdout); 42 scanf("%d",&n); 43 rep(i,1,3*n) inv[i]=ksm(i,mod-2); 44 rep(i,1,n) scanf("%d%d%d",&a[i],&b[i],&c[i]),s[i]=ksm(a[i]+b[i]+c[i],mod-2); 45 rep(i,2,n) scanf("%d%d",&u,&v),add(u,v,0),add(v,u,1); 46 dfs(1,1); 47 rep(i,1,sz[1]*3) inc(ans,f[1][i]); 48 printf("%d\n",ans); 49 return 0; 50 }