[冲刺国赛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);
}