[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 过程中维护容斥系数直接算答案

posted @ 2022-02-21 22:23  yyyyxh  阅读(46)  评论(0编辑  收藏  举报