[ARC101C] Ribbons on Tree & [CTS2019] 氪金手游(容斥+DP)
AT4352 [ARC101C] Ribbons on Tree
妙题,如果按照套路子树 DP 匹配子树内外的点 \(O(n^3)\)
但是剑走偏锋容斥一下,钦定若干条边一定不被覆盖,发现问题变成了若干个联通块任意配对方案数乘积
而一个大小为 \(n\) (\(n\) 为偶数)联通块任意匹配方案数为
\[g_n=\prod_{i=1}^{\frac{n}{2}} (2i-1)
\]
考虑用 DP 优化容斥,转移过程中维护容斥系数,\(f_{u,i}\) 表示 \(u\) 目前所在联通块大小为 \(i\) ,暴力卷起来转移即可
#include <cstdio>
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<22,stdin)),p1==p2?EOF:*p1++)
#pragma GCC optimize(2,3,"Ofast")
using namespace std;
char buf[1<<22],*p1=buf,*p2=buf;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int _=5003;
const int P=1000000007;
int hd[_],ver[_<<1],nxt[_<<1],tot;
void add(int u,int v){nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;}
int f[_][_],g[_],sz[_],t[_],n;
void dfs(int u,int fa){
sz[u]=1;f[u][1]=1;
for(int i=hd[u],v;i;i=nxt[i])
if((v=ver[i])^fa){
dfs(v,u);
for(int j=0;j<=sz[u];++j) t[j]=f[u][j],f[u][j]=0;
for(int j=1;j<=sz[u];++j)
for(int k=0;k<=sz[v];++k)
f[u][j+k]=(f[u][j+k]+1ll*t[j]*f[v][k])%P;
sz[u]+=sz[v];
}
for(int i=2;i<=sz[u];i+=2)
f[u][0]=(f[u][0]-1ll*g[i]*f[u][i]%P+P)%P;
}
int main(){
n=read();
for(int i=1;i<n;++i){
int u=read(),v=read();
add(u,v);add(v,u);
}
g[0]=1;for(int i=2;i<=n;i+=2) g[i]=1ll*g[i-2]*(i-1)%P;
dfs(1,0);
printf("%d\n",P-f[1][0]);
return 0;
}
[CTS2019] 氪金手游
大妙题,玄学题
开始考虑先把概率 DP 出来在树上容斥,按理说每次选取 \(W_i\) 的概率应该是独立的,但是就是会 \(WA\)
看了题解发现是和上面一题同样的套路(DP 优化容斥)
首先分式 \(\frac{W_i}{\sum_j W_j}\) 肯定不满足线性性,所以我们不得不把分母当成状态压进 DP 中,用类似上一题的 DP 转移
转化一下题意,发现原图就是一颗弱联通的树
对于一颗外向树来说,根节点要成为第一个被选的概率为 \(\frac{Pr(u)}{\sum_{v\in subtree(u)} Pr(v)}\) ,这个结合实际意义不难看出,用级数算也可以
那么把反向边容斥掉,在树 DP 时不断乘容斥系数
#include <cstdio>
using namespace std;
const int N=6003;
const int M=3000003;
const int P=998244353;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
int inv[M],n,ans;
int f[N][N*3],t[N*3];
int p[N][4];
int pr[N],sz[N];
int hd[N],ver[N<<1],nxt[N<<1],tot;
bool dir[N<<1];
void add(int u,int v,bool w){
nxt[++tot]=hd[u];hd[u]=tot;
ver[tot]=v;dir[tot]=w;
}
int qp(int a,int b=P-2){
int r=1;
while(b){
if(b&1) r=1ll*r*a%P;
a=1ll*a*a%P;
b>>=1;
}
return r;
}
void dfs(int u,int fa){
sz[u]=3;
f[u][1]=1ll*p[u][1]*p[u][0]%P;
f[u][2]=2ll*p[u][2]*p[u][0]%P;
f[u][3]=3ll*p[u][3]*p[u][0]%P;
for(int i=hd[u];i;i=nxt[i]){
int v=ver[i];
if(v==fa) continue;
dfs(v,u);
for(int j=1;j<=sz[u];++j) t[j]=f[u][j],f[u][j]=0;
for(int j=1;j<=sz[u];++j)
for(int k=1;k<=sz[v];++k){
int tmp=1ll*t[j]*f[v][k]%P;
if(dir[i]) f[u][j+k]=(f[u][j+k]+tmp)%P;
else f[u][j+k]=(f[u][j+k]+P-tmp)%P,f[u][j]=(f[u][j]+tmp)%P;
}
sz[u]+=sz[v];
}
for(int i=1;i<=sz[u];++i) f[u][i]=1ll*f[u][i]*inv[i]%P;
}
int main(){
n=read();inv[1]=1;ans=0;
for(int i=2;i<=3*n;++i) inv[i]=1ll*inv[P%i]*(P-P/i)%P;
for(int i=1;i<=n;++i){
p[i][1]=read();
p[i][2]=read();
p[i][3]=read();
p[i][0]=qp(p[i][1]+p[i][2]+p[i][3]);
}
for(int i=1;i<n;++i){
int u=read(),v=read();
add(u,v,1);add(v,u,0);
}
dfs(1,0);
for(int i=1;i<=3*n;++i) ans=(ans+f[1][i])%P;
printf("%d\n",ans);
return 0;
}
概率好玄学……
总结一下这两题的套路
当遇到对于一个联通块的答案容易计算,而断边形成联通块的方案较多(\(O(2^n)\))时,在 DP 过程中维护容斥系数直接算答案