MX 暑假集训 7.23

"FUTURE EVE" feat.初音未来 / sasakure.‌UK + 有形Rampage (フューチャー・イヴ feat.Miku Hatsune)

非常难的 dp 专题。

一些简单题

基本都是暴力 dp 或者比较基础的推式子,不讲了。

[HAOI2015] 树上染色
OSU!
[JSOI2018] 潜入行动
[NOI2009] 管道取珠

这题有个 trick 可以讲一下,每种状态的方案的平方可以看作得到这种状态的方案二元组的数量。

Tree Coloring

题意

给定一棵有根树,你需要将第 \(i\) 个节点染成颜色 \(c_i\),求有多少种排列 \(c\) 满足对于每个点 \(u\) 的颜色都不为它的父亲 \(fa_u\) 的颜色减去 \(1\),即 \(c_u\ne c_{fa_u}-1\)

\(1\le n\le 2.5\times 10^5\)

solution

容斥经典题。

考虑钦定一个点集 \(S\),使点集内的所有点 \(u\),都不满足条件,即 \(c_u= c_{fa_u}-1\),对于不在点集 \(S\) 的点,没有限制,那么答案就是 \(\sum\limits_{S 合法} (-1)^{\lvert S\rvert }(n- \lvert S\rvert)!\),其中乘上 \((n-\lvert S\rvert )!\) 的意义是,对于所有有限制的点,将其与父亲缩成一个点,则最终会形成 \(n-\lvert S\rvert\) 个连通块,那么每个连通块的权值可以任意排列,由于连通块内的大小已经完全确定(即一根由 \(S\) 内的点组成的链的顶端一定是最大值),所以系数应该是 \((n-\lvert S\rvert)!\),而非 \(A_{n}^{n-\lvert S\rvert}\)

因为一个节点只能由一个子节点在 \(S\) 内,而系数又只与 \(S\) 的大小相关,所以对于每个节点考虑它是否选择一个子节点加入 \(S\),令 \(s_u\)\(u\) 的子节点数量,那么计算出 \(\prod\limits_{i=1}^{n} (s_i x+1)\) 得到的多项式中 \(x^k\) 的系数即为 \(\lvert S\rvert =k\) 的合法 \(S\) 个数,直接上分治 NTT 即可做到 \(O(n\log ^2 n)\)

计算 \(\prod\limits_{i=1}^{n} (s_i x+1)\) 这个式子还有更优秀的做法。

由于 \(\sum s_i=n-1\),令满足 \(s_i=d\)\(i\) 共有 \(cnt_d\) 个,将子节点数相同的节点合并为 \((d x+1)^{cnt_d}\),用二项式展开,再将所有式子卷起来,一侧长度小就暴力乘,否则用 NTT,这个做法的时间复杂度神秘地达到了 \(O(n\log n)\),证明似乎可以用哈夫曼树。

Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<cmath>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

namespace Poly {
	constexpr int N=2e6+10;
	constexpr double pi=acos(-1.0);
	constexpr int mod=998244353,gen=3;
	
	inline ll qpow(ll a,int b) {
		ll res=1;
		while (b) {
			if (b&1) res=res*a%mod;
			a=a*a%mod;b>>=1;
		}
		return res;
	}
	
	static int len,rev[N];
	inline void initrev() {
		const int lt=log(len)/log(2)-1;
		f(i,1,len-1) rev[i]=((rev[i>>1]>>1)|((i&1)<<lt));
	}
	
	static int g[30],flag_init_gen;
	inline void init_gen() {f(i,1,22) g[i]=qpow(gen,(mod-1)>>i);}
	inline int ad(int x,int y) {return (x+y>=mod)?(x+y-mod):(x+y);}
	inline void add(int &x,int y) {x=ad(x,y);}
	
	inline void NTT(int *a,int type) {
		if (!flag_init_gen) init_gen(),flag_init_gen=1;
		f(i,1,len-1) if (rev[i]<i) swap(a[i],a[rev[i]]);
		for (int mid=1,t=1;mid<len;mid<<=1,t++) {
			int Wn=g[t];
			for (int i=mid<<1,j=0;j<len;j+=i) {
				int w=1;
				for (int k=0;k<mid;k++,w=(ll)(w)*Wn%mod) {
					int x=a[j+k],y=(ll)(w)*a[j+k+mid]%mod;
					a[j+k]=ad(x,y);a[j+k+mid]=ad(x,mod-y);
				}
			}
		}
		if (type==-1) {
			reverse(a+1,a+len);
			const ll invn=qpow(len,mod-2);
			f(i,0,len-1) a[i]=invn*a[i]%mod;
		}
	}
	
	inline void poly_px(int *f,int *g,int len_mx) {for(int i=0;i<len_mx;i++) f[i]=(ll)(f[i])*g[i]%mod;} 
	inline void poly_cpy(int *f,int *g,int len_mx) {for(int i=0;i<len_mx;i++) f[i]=g[i];}
	inline void poly_clr(int *f,int l,int r) {f(i,l,r) f[i]=0;}
	
	inline void NTT_mul(int *f,int *g,int *ret,int lenf,int leng) {
		static int X0[N],Y0[N];
		for (len=1;len<lenf+leng;len<<=1);
		initrev();
		poly_cpy(X0,f,lenf);
		poly_cpy(Y0,g,leng);
		NTT(X0,1);NTT(Y0,1);poly_px(X0,Y0,len);NTT(X0,-1);
		f(i,0,lenf+leng) ret[i]=X0[i];
		f(i,0,len) X0[i]=Y0[i]=0;
	}
}

constexpr int N=3e5+10;
constexpr int mod=998244353;
inline int ad(int x,int y) {return (x+y>=mod)?(x+y-mod):(x+y);}
inline void add(int &x,int y) {x=ad(x,y);}

int n;
int val[N],fac[N];

inline vector<int> mul(vector<int> X,vector<int> Y) {
	static int F[N],G[N],H[N];
	int lenf=X.size(),leng=Y.size(),lenh=lenf+leng-1;
	f(i,0,lenf-1) F[i]=X[i];
	f(i,0,leng-1) G[i]=Y[i];
	vector<int> ret;
	Poly::NTT_mul(F,G,H,lenf,leng);
	f(i,0,lenh-1) ret.push_back(H[i]);
	return ret;
}

inline vector<int> solve(int l,int r) {
	if (l==r) {
		vector<int> ret;
		ret.push_back(1);
		ret.push_back(val[l]);
		return ret;
	}
	int mid=l+r>>1;
	return mul(solve(l,mid),solve(mid+1,r));
}

int main() {
	read(n);fac[0]=1;
	f(i,2,n) {
		int x,y;read(x);read(y);
		val[x]++;val[y]++;
	}
	f(i,2,n) val[i]--;
	vector<int> ret=solve(1,n);
	f(i,1,n) fac[i]=1ll*fac[i-1]*i%mod;
	int ans=0;
	f(i,0,n-1) {
		int val=1ll*((i&1)?-1:1)*fac[n-i]*ret[i]%mod;
		if (val<0) val+=mod;
		add(ans,val);
	}
	printf("%d\n",ans);
	return 0;
}

Piling Up

题意

将共 \(n\) 个白球和黑球放入一个箱子中,每种颜色的球数没有限制,但要保证个数和为 \(n\),你需要进行 \(m\) 轮操作,每轮操作形式如下。

  • 先从箱子拿出任意一个球;
  • 再将一个白球和一个黑球放入箱子里;
  • 最后再从箱子拿出一个球。

求拿出球组成的颜色序列有多少种可能,答案对 \(10^9+7\) 取模。

\(1\le n,m\le 3\times 10^3\)

solution

首先想到一个很显然的 dp,令 \(f_{i,j}\) 表示进行了 \(i\) 次操作,箱子里有 \(j\) 个白球和 \(n-j\) 个黑球,得到序列的方案数。

这个转移很简单,但是这个 dp 会算重,考虑什么时候会算重。

\(i\) 看作横坐标,\(j\) 看作纵坐标,那么一个方案可以看成一条折线,两个方案得到的颜色序列相同当且仅当折线的形状相同,而初始的 \(j\) 不同(因为只需要保证和为 \(n\),所以 \(j\) 初始时可以是 \([0,n]\) 内的任意值),那么有个很牛的思想就是钦定代表元,这些折线既然形状相同,只是进行了上下平移,那么有且仅有一条折线会碰到 \(j=n\) 这条线(因为所有折线值域都为 \([0,n]\)),那么我们只计入碰到了 \(j=n\) 这条直线的折线方案数即可。

实现时可以再开一维表示是否碰到 \(j=n\),但是其实没必要,我们只需要将值域缩减到 \(n-1\) 再计算一次方案数,减去即可。

时间复杂度 \(O(nm)\)

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=3e3+10;
constexpr int inf=1e9+10;

constexpr int mod=1e9+7;
inline int ad(int x,int y) {return (x+y>=mod)?(x+y-mod):(x+y);}
inline void add(int &x,int y) {x=ad(x,y);}

inline int solve(int n,int m) {
	if (!n) return 0;
	static int dp[2][N];
	f(i,0,n) dp[0][i]=1;
	f(i,1,m) {
		const int d0=(i&1),d1=(d0^1);
		f(j,0,n) dp[d0][j]=0;
		f(j,0,n) {
			if (j!=0&&j!=n) {
				add(dp[d0][j],ad(dp[d1][j],dp[d1][j]));
				add(dp[d0][j-1],dp[d1][j]);
				add(dp[d0][j+1],dp[d1][j]);
			}
			if (j==0) {
				add(dp[d0][j],dp[d1][j]);
				add(dp[d0][j+1],dp[d1][j]);
			}
			if (j==n) {
				add(dp[d0][j],dp[d1][j]);
				add(dp[d0][j-1],dp[d1][j]);
			}
		}
	}
	int ret=0;
	f(i,0,n) add(ret,dp[m&1][i]);
	f(i,0,1) f(j,0,n) dp[i][j]=0;
	return ret;
}

int main() {
	int n,m;
	scanf("%d%d",&n,&m);
	printf("%d\n",(solve(n,m)-solve(n-1,m)+mod)%mod);
	return 0;
}

「LibreOJ NOI Round #2」不等关系

题意

给定一个长度为 \(n\),仅包含 \(<,>\) 的字符串 \(S\),求有多少个长度为 \(n+1\) 的排列 \(p\) 满足对于任意 \(1\le i\le n\),若 \(S_i\)\(<\)\(p_i < p_{i+1}\),否则 \(p_i > p_{i+1}\)

\(1\le n\le 10^5\)

solution

很经典的容斥。

考虑不管 \(>\) 的限制,只考虑 \(<\) 怎么做。

只考虑 \(<\) 的限制时简单的,只需要简单组合计数一下即可算出答案,加上 \(>\) 号会很麻烦,所以我们考虑容斥掉 \(>\) 的限制。

钦定一个集合 \(S\),满足 \(S\) 内的 \(>\) 号一定不能满足,即对于 \(S\) 内的 \(>\) 号,若其在第 \(i\) 个位置,那么在实际的排列上有 \(p_i<p_{i+1}\)

\(f(S)\) 表示让 \(S\) 内的 \(>\) 号一定不满足,其他 \(>\) 号没有限制的方案数,根据容斥,答案即为 \(\sum (-1)^{\left \lvert S \right \rvert}f(S)\)

考虑用 dp 去维护这个容斥,我们关心的的其实是使用不在 \(S\) 内的 \(>\) 号进行分割后得到的若干部分的长度(分割后每个部分形成一个递增的序列,即段内全部满足 \(<\) 的限制,这里的 \(<\) 包括 \(S\) 内的 \(>\)),令 \(dp_i\) 表示考虑第 \(i\) 个符号前的容斥和,那么我们枚举上一个不出现在 \(S\) 内的 \(>\) 号,令位置为 \(j\),则 \((j,i)\) 内所有 \(>\) 号都在 \(S\) 内,并且排列在 \([j,i)\) 上单调递增,所以我们可以列出转移方程如下:

\[dp_i \leftarrow [s_j='>'] (-1)^{cnt_{i-1}-cnt_j} \binom{i}{i-j} dp_j \]

其中,\(cnt_i\) 表示 \([1,i]\)\(s_i='>'\)\(i\) 的个数,\((-1)^{cnt_{i-1}-cnt_j}\) 表示了将 \((j,i)\) 内所有 \(>\) 号选入集合 \(S\) 所带来的容斥系数,\(\binom{i}{i-j}\) 表示了从 \(i\) 个数中选出 \(i-j\) 个数构成 \([j,i)\) 上的递增序列。

于是我们得到了一个 \(O(n^2)\) 的 dp 算法。

后面就是优化了,把转移方程两组合数拆成阶乘形式,并在两边同时除以 \(i!\),即可得到:

\[\frac{dp_i}{i!} \leftarrow [s_j='>'] (-1)^{cnt_{i-1}-cnt_j} \frac{1}{(i-j)!} \frac{dp_j}{j!} \]

这就是一个喜闻乐见的卷积形式了,用分治 NTT 即可做到 \(O(n\log^2 n)\)

Code
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<cmath>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=4e5+10;
constexpr int inf=1e9+10;

namespace Poly {
	constexpr int N=2e6+10;
	constexpr double pi=acos(-1.0);
	constexpr int mod=998244353,gen=3;
	
	inline ll qpow(ll a,int b) {
		ll res=1;
		while (b) {
			if (b&1) res=res*a%mod;
			a=a*a%mod;b>>=1;
		}
		return res;
	}
	
	static int len,rev[N];
	inline void initrev() {
		const int lt=log(len)/log(2)-1;
		f(i,1,len-1) rev[i]=((rev[i>>1]>>1)|((i&1)<<lt));
	}
	
	static int g[30],flag_init_gen;
	inline void init_gen() {f(i,1,22) g[i]=qpow(gen,(mod-1)>>i);}
	inline int ad(int x,int y) {return (x+y>=mod)?(x+y-mod):(x+y);}
	inline void add(int &x,int y) {x=ad(x,y);}
	
	inline void NTT(int *a,int type) {
		if (!flag_init_gen) init_gen(),flag_init_gen=1;
		f(i,1,len-1) if (rev[i]<i) swap(a[i],a[rev[i]]);
		for (int mid=1,t=1;mid<len;mid<<=1,t++) {
			int Wn=g[t];
			for (int i=mid<<1,j=0;j<len;j+=i) {
				int w=1;
				for (int k=0;k<mid;k++,w=(ll)(w)*Wn%mod) {
					int x=a[j+k],y=(ll)(w)*a[j+k+mid]%mod;
					a[j+k]=ad(x,y);a[j+k+mid]=ad(x,mod-y);
				}
			}
		}
		if (type==-1) {
			reverse(a+1,a+len);
			const ll invn=qpow(len,mod-2);
			f(i,0,len-1) a[i]=invn*a[i]%mod;
		}
	}
	
	inline void poly_px(int *f,int *g,int len_mx) {for(int i=0;i<len_mx;i++) f[i]=(ll)(f[i])*g[i]%mod;} 
	inline void poly_cpy(int *f,int *g,int len_mx) {for(int i=0;i<len_mx;i++) f[i]=g[i];}
	inline void poly_clr(int *f,int l,int r) {f(i,l,r) f[i]=0;}
	
	inline void NTT_mul(int *f,int *g,int *ret,int lenf,int leng) {
		static int X0[N],Y0[N];
		for (len=1;len<lenf+leng;len<<=1);
		initrev();
		poly_cpy(X0,f,lenf);
		poly_cpy(Y0,g,leng);
		NTT(X0,1);NTT(Y0,1);poly_px(X0,Y0,len);NTT(X0,-1);
		f(i,0,lenf+leng) ret[i]=X0[i];
		f(i,0,len) X0[i]=Y0[i]=0;
	}
}

constexpr int mod=998244353;
inline int ad(int x,int y) {return ((x+y>=mod)?(x+y-mod):(x+y));}
inline void add(int &x,int y) {x=ad(x,y);}

inline ll qpow(ll a,int b) {
	ll res=1;
	while (b) {
		if (b&1) res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

ll fac[N],inv[N];
inline void init(int n) {
	fac[0]=1;
	f(i,1,n) fac[i]=fac[i-1]*i%mod;
	inv[n]=qpow(fac[n],mod-2);
	F(i,n,1) inv[i-1]=inv[i]*i%mod;
}

inline ll C(int n,int m) {
	if (n<0||m<0||n<m) return 0;
	return fac[n]*inv[m]%mod*inv[n-m]%mod;
}

inline vector<int> NTT(vector<int> &x,vector<int> &y) {
	int lenx=x.size(),leny=y.size();
	static int X[N],Y[N],H[N];
	f(i,0,lenx-1) X[i]=x[i];
	f(i,0,leny-1) Y[i]=y[i];
	Poly::NTT_mul(X,Y,H,lenx,leny);
	int lenh=lenx+leny-1;
	vector<int> ret;
	f(i,0,lenh-1) ret.push_back(H[i]);
	return ret;
}

int n;
int c[N],dp[N];
char s[N];

inline void solve(int l,int r) {
	if (l==r) return;
	int mid=l+r>>1;
	solve(l,mid);
	vector<int> vx,vy;
	f(i,l,mid) vx.push_back((s[i]=='<')?0:((c[i]&1)?(mod-dp[i]):dp[i]));
	f(i,0,r-l+1) vy.push_back(inv[i]);
	vector<int> ret=NTT(vx,vy);
	f(i,mid+1,r) {
		if (c[i-1]&1) add(dp[i],mod-ret[i-l]);
		else add(dp[i],ret[i-l]);
	}
	solve(mid+1,r);
}

int main() {
	init(N-1);
	scanf("%s",s+1);
	n=strlen(s+1);
	f(i,1,n+1) c[i]=c[i-1]+(s[i]=='>');
	dp[0]=1;solve(0,n+1);
	printf("%d\n",(int)(dp[n+1]*fac[n+1]%mod));
	return 0;
}

[SNOI2024] 公交线路

题意

给定一棵 \(n\) 个节点的树,你需要在一些点对间的简单路径上建立公交线路,使得每两个点间至多只需换乘一次公交线路就能抵达,求方案数。

形式化地说,考虑树上的所有 \(\frac{n (n - 1)}{2}\) 条两个端点不同的简单路径。对于这些路径的一个子集 \(S\),称它是好的当且仅当:

  • 考虑一张新的图 \(G\),对于一对点 \(u, v\),当且仅当存在 \(S\) 中的一条路径 \(P\),满足 \(u\)\(v\) 都在 \(P\) 上,我们会在 \(u, v\) 之间连上边权为 \(1\) 的无向边。
  • 要求 \(G\) 中任意两点之间的距离都不超过 \(2\)

你需要求出有多少个子集 \(S\) 是好的。由于答案可能很大,输出对 \(998244353\) 取模的结果。

\(1\le n\le 3000\)

solution

神秘的容斥,做完发现欸我怎么在天上飞。

首先 \(n\le 2\) 时答案为 \(1\),以下只考虑 \(n\ge 3\) 的情况,首先找到一个非叶子节点作根。

每两个点间至多换乘一次的限制只需要考虑叶子节点是否满足即可,而叶子节点满足条件当且仅当考虑叶子节点直接经过的路径并集,所有叶子覆盖路径的交为一个连通块,这是显然的。

对连通块相关计数,由树的性质结合平面图欧拉定理可得 \(V-E=1\),所以可以变成对点减边计数,即令 \(F_x\) 为点 \(x\) 被所有叶子节点所选的路径共同覆盖(即包含在交集中)的方案数,\(G_e\) 为边 \(e\) 被所有叶子路径覆盖方案数,那么答案即为 \(\sum F_x -\sum G_e\)

接下来需要计算 \(F_x\)\(G_e\)

\(F_x\) 似乎不是很好直接计算,于是考虑容斥,钦定一个叶子节点的集合 \(S\),使得 \(S\) 内的所有叶子节点直接经过的路径都不经过 \(x\),其他叶子没有限制,令这样的方案数为 \(f(S)\),那么 \(F_x=\sum (-1)^{\left \lvert S\right \rvert} f(S)\)

考虑进行一个 dp,令 \(dp_i\) 表示 \(x\) 子树内有 \(i\) 个点没有限制(非叶子节点或叶子节点且不在 \(S\) 内)的方案数,这里的方案数只考虑了跨过 \(x\) 的路径,其他的路径不会计入 dp 内,加入一棵子树 \(v\),令 \(v\)\(siz_v\) 个节点(包括叶子),\(lef_v\) 个叶子节点,那么枚举 \(v\) 中有 \(j\) 个叶子节点加入 \(S\) 内,有以下转移:

\[dp_{i+siz_{v}-j}\leftarrow (-1)^j \times \binom{lef_v}{j} \times 2^{i\times (siz_v-j)} \times dp_i \]

其中 \((-1)^j\) 是容斥系数,\(\binom{lef_v}{j}\) 表示在 \(v\) 子树内选出 \(j\) 个叶子,\(2^{i\times (siz_v-j)}\) 表示 \(v\) 子树内无限制节点对于其他子树的无限制节点之间的路径可以选或不选,然后无限制点数变成 \(i+siz_v-j\)

求出子树内的 dp 后,还需要考虑父亲方向的部分带来的贡献,父亲方向就不用容斥了,只需要要求其所有叶子节点直接经过路径经过了 \(x\) 即可。

那么令父亲方向一整块有 \(siz_p\) 个节点,\(lef_p\) 个叶子,那么有以下式子:

\[F_x=2^{\binom{siz_p}{2} + \sum\limits_v \binom{siz_v}{2}} \times \sum\limits_{i} dp_i \times 2^{i\times (siz_p-lef_p)} \times (2^i-1)^{lef_p} \]

其中,\(2^{\binom{siz_p}{2} + \sum\limits_v \binom{siz_v}{2}}\) 表示,不跨过 \(x\) 的路径可以任选,\(2^{i\times (siz_p-lef_p)}\) 表示父亲方向的无限制节点与 \(x\) 子树内无限制节点之间的路径可以任选,\((2^i-1)^{lef_p}\) 表示父亲方向的每个叶子节点与 \(x\) 子树内的无限制节点路径中至少要选一条。

那么 \(F_x\) 做完了,那么考虑 \(G_e\) 怎么求。

实际上和 \(F_x\) 的求法很类似,对于一条边 \(e\),令其连接了 \(u,v\) 两个节点,这两个方向的节点数量和叶子数量分别为 \(siz,lef\),那么枚举 \(u\) 这一侧的限制节点数(在 \(S\) 内的节点数)即可得到:

\[G_e = 2^{\binom{siz_u}{2}+\binom{siz_v}{2}} \times \sum\limits_i (-1)^{i} \times \binom{lef_u}{i} \times 2^{(siz_u-i)\times (siz_v-lef_v)} \times (2^{siz_u-i}-1)^{lef_v} \]

各项系数和上面求点的系数几乎一样,不再解释了。

预处理出各种幂和组合数,树上背包复杂度 \(O(n^2)\),所以计算点贡献 \(O(n^2)\),每条边 \(O(n)\) 计算贡献,所以计算边贡献 \(O(n^2)\)

总时间复杂度 \(O(n^2)\),空间复杂度 \(O(n^2)\),空间瓶颈在于预处理出的幂数组。

Code
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=3000+10;
constexpr int inf=1e9+10;

constexpr int mod=998244353;
inline int ad(int x,int y) {return ((x+y>=mod)?(x+y-mod):(x+y));}
inline void add(int &x,int y) {x=ad(x,y);}

inline ll qpow(ll a,int b) {
	ll res=1;
	while (b) {
		if (b&1) res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

ll fac[N],inv[N],pw2[N*N],pw0[N][N];
inline void init(int n,int nn) {
	fac[0]=pw2[0]=1;
	f(i,1,n) pw2[i]=ad(pw2[i-1],pw2[i-1]);
	n=nn;
	f(i,1,n) fac[i]=fac[i-1]*i%mod;
	inv[n]=qpow(fac[n],mod-2);
	F(i,n,1) inv[i-1]=inv[i]*i%mod;
	f(i,1,n) {
		ll mul=ad(pw2[i],mod-1);
		pw0[i][0]=1;
		f(j,1,n) pw0[i][j]=pw0[i][j-1]*mul%mod; 
	}
}

inline ll C(int n,int m) {
	if (n<0||m<0||n<m) return 0;
	return fac[n]*inv[m]%mod*inv[n-m]%mod;
}

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,sum_lef;
vector<int> e[N];

ll ans;
int siz[N],lef[N];
int f[N],g[N];

inline void dfs(int now,int fa) {
	siz[now]=1;
	lef[now]=(e[now].size()==1);
	int Cs=0;
	for (int v:e[now]) if (v^fa) dfs(v,now);
	f[1]=1;
	for (int v:e[now]) if (v^fa) {
		f(i,1,siz[now]) g[i]=f[i],f[i]=0;
		f(i,1,siz[now]) f(j,0,lef[v]) {
			if (j&1) add(f[i+siz[v]-j],mod-C(lef[v],j)*pw2[i*(siz[v]-j)]%mod*g[i]%mod);
			else add(f[i+siz[v]-j],C(lef[v],j)*pw2[i*(siz[v]-j)]%mod*g[i]%mod);
		}
		siz[now]+=siz[v];
		lef[now]+=lef[v];
		Cs+=siz[v]*(siz[v]-1)/2;
	}
	int sF=n-siz[now],lF=sum_lef-lef[now];
	Cs+=sF*(sF-1)/2;
	ll sum=0;
	f(i,1,siz[now]) {
		sum+=f[i]*pw0[i][lF]%mod*pw2[i*(sF-lF)]%mod;
		f[i]=0;
	}
	ans+=(sum%mod)*pw2[Cs]%mod;
	if (!fa) return;
	sum=0;
	f(i,0,lef[now]) {
		int k=siz[now]-i;
		sum+=((i&1)?-1:1)*pw2[k*(sF-lF)]*pw0[k][lF]%mod*C(lef[now],i)%mod;
	}
	ans-=sum%mod*pw2[sF*(sF-1)/2+siz[now]*(siz[now]-1)/2]%mod;
}

int main() {
	read(n);init(n*n,n);
	if (n<=2) return puts("1"),0;
	f(i,2,n) {
		int u,v;read(u);read(v);
		e[u].push_back(v);
		e[v].push_back(u);
	}
	f(i,1,n) sum_lef+=(e[i].size()==1);
	f(i,1,n) if (e[i].size()>=2) {dfs(i,0);break;}
	printf("%d\n",(int)((ans%mod+mod)%mod));
	return 0;
}

习题

容斥

Same Descent Set

Min Product Sum

[集训队互测 2023] 基础寄术练习题

slope trick 及决策单调性

[APIO2016] 烟花表演

[ICPC 2016 NAIPC] Jewel Thief

A New Beginning

posted @ 2026-07-24 10:19  Ff472130  阅读(14)  评论(0)    收藏  举报