[冲刺国赛2022] 数叶子

一、题目

有一棵 \(n\) 个点的树,每条边被分配了一个在 \(1,2...m\) 中的整数作为编号,且这些编号两两不同。

对于所有的标号方式,求出这个问题的答案的和:保留编号在 \([l,r]\)\(1\leq l\leq r\leq m\))的边构成的森林中,有多少个点是叶子。(在本题中,叶子的定义为有恰好一个相邻点的节点)

\(n\leq 10^6,m\leq 10^9\)

二、解法

可以用简单的贡献法写出答案,我们枚举区间长度 \(i\) 和作为叶子贡献的点 \(u\)

\[\sum _{i=1}^m (m-i+1)\sum_{u=1}^n d_u\cdot i\cdot A(m-i,d_u-1)\cdot A(m-d_u,n-1-d_u) \]

把上面的式子变一下形,可以得到:

\[\sum_{u=1}^n d_u\cdot A(m-d_u,n-1-d_u)\sum_{i=1}^m i\cdot A(m-i+1,d_u) \]

所以问题变成了,对于每个度数 \(d\) 求出:

\[\sum_{i=1}^m i\cdot A(m-i+1,d)=\sum_{i=1}^m i(m-i+1)^{\underline d} \]

\(i\) 当成自变量,在脑海中展开上式,发现它是一个关于 \(m\)\(d+2\) 次多项式。因为 \(m<d\) 时点值为 \(0\),所以只需要暴力求出剩下的点值,然后用 \(O(n)\) 的拉格朗日插值法即可解决问题。


组合意义保平安,我们把枚举区间和分配标号两部分放在一起计数。问题变成了,有 \(m+2\) 个点,要从中选出 \(d+2\) 个点,其中两个点代表了区间 \([l,r]\)(指定标号的左右两个点),再乘上分配标号的 \(d!\) 即可:

\[\sum_{i=1}^m i\cdot A(m-i+1,d)={m+2\choose d+2}\cdot d! \]

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

#include <cstdio>
const int M = 1000005;
const int MOD = 1004535809;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,d[M],b[M],inv[M],a[M];
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
signed main()
{
	freopen("leaf.in","r",stdin);
	freopen("leaf.out","w",stdout);
	n=read();m=read();
	for(int i=1;i<n;i++)
		d[read()]++,d[read()]++;
	for(int i=1;i<=n;i++) b[d[i]]++;
	//
	inv[0]=inv[1]=1;
	for(int i=2;i<=n+1;i++)
		inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	a[0]=((m+2)*(m+1)>>1)%MOD;
	for(int i=1;i<n;i++)
		a[i]=a[i-1]*(m+1-i)%MOD*inv[i+2]%MOD;
	//
	int f1=1,f2=1,ans=0;
	for(int i=0;i<n-1;i++) f2=f2*(m-i)%MOD;
	for(int i=1;i<n;i++)
	{
		f1=f1*i%MOD;f2=f2*qkpow(m-i+1,MOD-2)%MOD;
		ans=(ans+b[i]*f1%MOD*f2%MOD*i%MOD*a[i])%MOD;
	}
	printf("%lld\n",ans);
}
posted @ 2022-07-05 16:07  C202044zxy  阅读(174)  评论(0编辑  收藏  举报