Prufer 序列
Tree \(\rightarrow\) Prufer
-
每次找到编号最小的叶子结点,在序列中添加其父亲。
-
删除该节点。
-
重复如上操作,得到长为 \(n-2\) 的序列。
还原同理。
Prufer 序列是 \(n\) 个点的完全图的生成树与一个长为 \(n-2\),值域 \(\lbrack 1,n\rbrack\) 的数列构成的双射。
P6086 【模板】Prüfer 序列
int f[N],p[N],d[N];
void Prufer(){
for(int i=1;i<n;i++)
f[i]=read(),d[f[i]]++;
for(int i=1,j=1;i<=n-2;i++,j++){
while(d[j])j++;
p[i]=f[j];
while(i<=n-2&&!(--d[p[i]])&&p[i]<j)
p[i+1]=f[p[i]],i++;
}
}
void Tree(){
for(int i=1;i<=n-2;i++)
p[i]=read(),d[p[i]]++;
p[n-1]=n;
for(int i=1,j=1;i<n;i++,j++){
while(d[j])j++;
f[j]=p[i];
while(i<n&&!(--d[p[i]])&&p[i]<j)
f[p[i]]=p[i+1],i++;
}
}
简单结论
-
prufer 序列与无根树一一对应。
-
一个点的在 prufer 序列中的出现次数为其度数 \(-1\).
-
\(n\) 个点的完全图的生成树个数为 \(n^{n-2}\).
prufer 序列长为 \(n-2\),每一位都有 \(n\) 种可能性。
- 度数为 \(d_{1\sim n}\) 的无根树共有 \(\displaystyle\frac{(n-2)!}{\prod_{i=1}^{n}(d_i-1)!}\) 种。
即可重全排列公式。
P2290 [HNOI2004] 树的计数
int main(){
cin>>n;
for(int i=1;i<=n;i++)
cin>>d[i],all+=d[i]-1;
if(n==1){
printf("%d\n",!d[1]);
return 0;
}
if(all!=n-2){
puts("0");return 0;
}
for(int i=1,sum=n-2;i<=n;i++)
ans*=C[sum][d[i]-1],sum-=d[i]-1;
printf("%lld\n",ans);
}
P2624 [HNOI2008] 明明的烦恼
一些点的度数不确定。\(1\le n\le 1000\).
记已知的个数为 \(k\),\(\sum d_i-1=s\).
答案为
牛马高精题。
洛阳七天下大雨同学附赠的 operator:
struct Sdate{
vector<LL> mx;
} A;
Sdate operator * (const Sdate A,const Sdate B){
int l1=A.mx.size(),l2=B.mx.size(),l3=l1+l2-1;
Sdate C; C.mx.resize(l1+l2-1);
for(int i=0;i<l1;i++)
for(int j=0;j<l2;j++)
C.mx[i+j]+=A.mx[i]*B.mx[j];
for(int i=0;i<l3;i++){
if(i!=l3-1) C.mx[i+1]+=C.mx[i]/10;
else if(C.mx[i]/10) C.mx.push_back(C.mx[i]/10),l3++;
C.mx[i]%=10;
}
return C;
}
这下能做了。
暴力分解质因数最后乘起来。
写错了个逆天细节。
Clues
带标号无向图,记其有 \(k\) 个连通块,求用 \(k-1\) 条边使其连通的方案数,答案对 \(p\) 取模。
\(1\le n,m\le 10^5\),\(1\le p\le 10^9\).
等价缩点,记连通块 \(i\) 的点数为 \(s_i\),度数为 \(d_i\).
prufer 序列的总数为 \(\displaystyle\frac{(k-2)!}{\prod_{i=1}^{k}(d_i-1)!}\).
使连通需要再乘上 \(\displaystyle\prod_{i=1}^{k}s_i^{ d_i}\).
设 \(e_i=d_i-1\):
多元二项式定理,即
答案为
使用并查集维护 \(k\) 和 \(s\).
P5437 【XR-2】约定
\(n\) 个点的无向完全图,\(i,j\) 间的边权为 \((i+j)^k\),求生成树的期望权值和。
答案对 \(998244353\) 取模。
\(1\le n\le 998244352\),\(1\le k\le 10^7\).
生成树共 \(n^{n-2}\) 个,共 \((n-1)n^{n-2}\) 条边,所以每条边出现 \(\displaystyle\frac{(n-1)n^{n-2}}{\frac{1}{2}n(n-1)}=2n^{n-3}\) 次。
答案为
记
作差分得
容易发现 \(a_n\) 应该是关于 \(n\) 的 \(k+2\) 次多项式。
那么求出暴力前 \(k+3\) 项和插值都可以做到 \(O(k)\).
实际上我疏忽了一个问题。。。\(O(n)\) 拉插的逆元其实是多带了一只 \(\log\) 的,所以模板题里面有可能只实现了 \(O(n^2)\) 转移到 \(O(n\log P)\).
为了优化这个逆元的常数,可以使用 P5431 【模板】乘法逆元 2 的套路:
假设当前要对 \(\{a\}\) 对 \(p\) 求逆,令 \(\displaystyle A=\prod_{i=1}^{n}a_i\),求出 \(A^{-1}\),那么有
维护前、后缀积即可。
最终答案为 \(\displaystyle\frac{2}{n}\cdot a_n\),时间复杂度 \(O(k)\).
附一份代码吧。
void init(){
f[1]=fac[0]=1;
for(int i=2;i<(R<<1);i++){
if(!vis[i])p[++cnt]=i,f[i]=qpow(i,k);
for(int j=1;j<=cnt&&i*p[j]<(R<<1);j++){
vis[i*p[j]]=true,f[i*p[j]]=1ll*f[i]*f[p[j]]%P;
if(i%p[j]==0)break;
}
}
for(int i=1;i<(R<<1);i++)
(f[i]+=f[i-1])%=P;
for(int i=1;i<=R;i++){
a[i]=((a[i-1]+f[i*2-1]-f[i])%P+P)%P;
fac[i]=1ll*fac[i-1]*i%P;
}
}
int b[N<<1],suf[N<<1],tot;
int calc(int n){
if(n<=R)return a[n];
int A=1,ret=0,r1,r2;
for(int i=1;i<=R;i++){
A=1ll*A*(n-i)%P,b[++tot]=n-i;
b[++tot]=(1ll*fac[i-1]*fac[R-i]%P*((R-i)&1?-1:1)+P)%P;
}
suf[tot+1]=1;
for(int i=tot;i;i--)
suf[i]=1ll*suf[i+1]*b[i]%P;
suf[0]=qpow(suf[1],P-2);
for(int i=1,pref=1,tp;i<=tot;i++){
tp=b[i];
b[i]=1ll*suf[0]*suf[i+1]%P*pref%P;
pref=1ll*pref*tp%P;
}
for(int i=1;i<=R;i++){
r1=1ll*A*b[i*2-1]%P,r2=b[i*2];
(ret+=1ll*a[i]*r1%P*r2%P)%=P;
}
return ret;
}
int main(){
R=k+3;
init();
ans=2ll*calc(n)%P*qpow(n,P-2)%P;
return 0;
}