Prufer序列学习笔记

Prufer序列是一种神奇的东西,其可以实现无根树与序列间的双射,并且在计数题、DP题、找规律题等等问题中有着不俗的表现。

(另,大部分时候,Prufer及其误拼Purfer、Purfur、Prefer等奇奇怪怪的变体是被混用了的)


首先,一棵 n 个节点的树的Prufer序列,是一长度为 n2 的序列。

实现树往Prufer序列的转化,步骤是不断找到当前所有叶子中编号最小的那一个,然后删除该叶子,并将该叶子所连接的节点加入序列。重复此操作,直到树中仅剩两个节点。


然后,实现Prufer序列往树的转化,步骤是顺次遍历Prufer序列,找到当前所有不在之后的Prufer序列里且尚未被选择的点中最小的那一个,并连接该点与当前点。

I.【模板】Prufer 序列

就是模板啦。两个算法都可以用一个指针维护当前所有合法点中最小的那一个,然后每做一次操作后,判断新产生的那个值是否比当前指针还要小,若是则采用新值即可。

时间复杂度均为 O(n)


但是,上述算法在知道有关Prufer序列的一些性质后会更加易于实现:

  1. 一个数在Prufer序列里的出现次数是其度数减一——这可以通过构建序列的算法来看出。同时,因为总度数是 2n2,故Prufer序列长度为 (2n2)n=n2。这也可以发现,叶节点并不会在序列中出现。

  2. 在树转序列的过程中,最终剩余的两个点中一定有一个是 n——因为无论何时,树中总有至少两个叶子,而 n 永远不会成为里面较小的一个,故永远不会被删去。

  3. 在序列转树的过程中,最终剩余两个点中还有一个是 n

有了这些性质,算法会非常好写。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll calc(int *a,int n){
	ll ret=0;
	for(int i=1;i<=n;i++)ret^=1ll*a[i]*i;
	return ret;
}
const int N=5e6+3;
int T,n;
namespace TreeToPrufer{
	int fa[N],cnt,pru[N],sz[N];
	ll func(){
		for(int i=1;i<n;i++)scanf("%d",&fa[i]),sz[fa[i]]++,sz[i]++;
		for(int i=1;i<n;i++){
			while(i<n&&sz[i]!=1)i++;
			if(i==n)break;
			for(int j=i;sz[j]==1&&j<=i;pru[++cnt]=fa[j],sz[fa[j]]--,j=fa[j]);
		}
		return calc(pru,n-2);
	}
}
namespace PruferToTree{
	int pru[N],fa[N],cnt[N];
	ll func(){
		for(int i=1;i<=n-2;i++)scanf("%d",&pru[i]),cnt[pru[i]]++;
		for(int i=1,j=1;j<=n-2;i++){
			while(i<=n&&cnt[i])i++;
			fa[i]=pru[j],cnt[pru[j]]--,j++;
			for(;j<=n-2&&!cnt[pru[j-1]]&&pru[j-1]<i;j++)fa[pru[j-1]]=pru[j],cnt[pru[j]]--;
		}
		for(int i=1;i<n;i++)if(!fa[i])fa[i]=n;
		return calc(fa,n-1);
	}
}
int main(){
	scanf("%d%d",&n,&T);
	if(T==1)printf("%lld\n",TreeToPrufer::func());
	else printf("%lld\n",PruferToTree::func());
	return 0;
} 

II.[HNOI2004]树的计数

可以被证明的是,任意一长度为 n2,且所有元素均在 [1,n] 内的序列,均是一合法的Prufer序列。同时,全部上述序列,就是全部合法的Prufer序列。因此,Prufer序列,即点数为 n 的树的数量,为 nn2

那在这题中,因为我们前面已经发现一度数为 i 的点的出现次数为 i1,而任何一种出现次数的排列均是一合法序列,故总方案数即为一多项式系数 (n2d11,d21,,dn1)

注意本题如果直接套多项式系数会爆炸,但是因为答案在 1017 以内,故可以设两个模数 109+7,109+9 然后求出模意义下的结果然后用CRT并一起。复杂度 O(n)

代码:

#include<bits/stdc++.h>
using namespace std;
int inv(int x,int y){
	int z=1,t=y-2;
	for(;t;t>>=1,x=1ll*x*x%y)if(t&1)z=1ll*z*x%y;
	return z;
}
const int m1=1e9+7,m2=1e9+9;
const int iv1=inv(m1,m2);
int n,r1,r2,f1[200],f2[200],i1[200],i2[200],tot;
int main(){
	scanf("%d",&n);
	f1[0]=f2[0]=1;for(int i=1;i<=n;i++)f1[i]=1ll*f1[i-1]*i%m1,f2[i]=1ll*f2[i-1]*i%m2;
	i1[n]=inv(f1[n],m1),i2[n]=inv(f2[n],m2);
	for(int i=n-1;i>=0;i--)i1[i]=1ll*i1[i+1]*(i+1)%m1,i2[i]=1ll*i2[i+1]*(i+1)%m2;
	r1=f1[n-2],r2=f2[n-2];
	for(int i=1,t;i<=n;i++)scanf("%d",&t),tot+=t,r1=1ll*r1*i1[t-1]%m1,r2=1ll*r2*i2[t-1]%m2;
	if(tot!=2*n-2){puts("0");return 0;}
	if(n==1){puts("1");return 0;}
	printf("%lld\n",(1ll*(r2-r1+m2)%m2*iv1%m2*m1+r1)%(1ll*m1*m2));
	return 0;
}

III.[清华集训2017]生成树计数

考虑令全体 di 减一,变成Prufer序列中每个块的出现次数。

先不考虑后面一大坨的 ,得到式子 ai{d}(n2)!di!aidi(di+1)m

前面是常数可以忽略。中间那个多项式系数的形式一看就知道要用EGF。于是我们设 Fi(x)=xkk!aik(k+1)m。则上式的EGF即为 Fi(x)

发现 xai 有相同的次数。故若我们设 F(x)=xkk!(k+1)m,则 Fi(x)=F(aix)。则上式即为 (ai)(n2)!([xn2]F(aix))

现在考虑 F(x)=xkk!w(k+1),其中 w(x) 是任意关于 x 的多项式。明显其是 F 的推广(F 中的 w(x)=xm),且上述关于 F 的推论仍然成立。

现在考虑让 回来,即为式子 (di+1)m(di+1)m。将 移到前面,就会发现其每次是令某个 i 不贡献 dim 而变成 di2m。于是我们令 w(x)=x2m,就得到了 F(x)=xkk!w(k+1)

于是我们要求的式子就是 F(aix)F(aix)F(aix)

现在我们考虑将其拆作两半计算:F(aix)F(aix)F(aix)。先来看前一半。

明显,求 ln 后再 exp 回去是其常规操作。于是问题变为 (lnF)(aix) 的求解。

考虑若有个多项式 P,要求出 P(aix)。若 P=pixi,则即要求 ijpjaijxj=j(pjiaij)xj。则若我们对于每个 j 都算出 iaij,则将其与 p 逐位相乘即可。

考虑其生成函数 ijaijxj,其等于 i11aix。通分即得 iji(1ajx)1aix

分母上的式子(设为 Q)可以用分治+FFT(不是分治FFT)O(nlog2n) 求出。分子上式子可以通过展开分子与分母上式子,瞪眼得到k=0n1xk(nk)[xk]Q(x),也可以简单求出。故 P(aix) 是可以求的;则 (lnF)(aix) 亦可简单求;则 F(aix) 求解完毕。

下面来看 F(aix)F(aix) 这个函数。发现,只需要令 P=FF 即可如上一样求解。

总复杂度 O(nlog2n)

好久没写多项式题了,板子都要debug好久了,废了废了

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int G=3;
const int N=1<<17;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
namespace Poly{
	int A[N],B[N],rev[N],lim,invlim,C[N],D[N];
	void NTT(int *a,int tp){
		for(int i=0;i<lim;i++)if(rev[i]>i)swap(a[i],a[rev[i]]);
		for(int md=1;md<lim;md<<=1){
			int rt=ksm(G,(mod-1)/(md<<1));
			if(tp==-1)rt=ksm(rt);
			for(int stp=md<<1,pos=0;pos<lim;pos+=stp)for(int i=0,w=1;i<md;i++,w=1ll*w*rt%mod){
				int x=a[pos+i],y=1ll*a[pos+md+i]*w%mod;
				a[pos+i]=(x+y)%mod;
				a[pos+md+i]=(x+mod-y)%mod;
			}
		}
		if(tp==-1)for(int i=0;i<lim;i++)a[i]=1ll*a[i]*invlim%mod;
	}
	void mul(int*a,int*b,int*c,int LG){
		lim=1<<LG,invlim=ksm(lim);
		for(int i=0;i<lim;i++)rev[i]=(rev[i>>1]>>1)|((i&1)<<(LG-1));
		for(int i=0;i<lim;i++)A[i]=B[i]=0;
		for(int i=0;i<(lim>>1);i++)A[i]=a[i],B[i]=b[i];
		NTT(A,1),NTT(B,1);
		for(int i=0;i<lim;i++)A[i]=1ll*A[i]*B[i]%mod;
		NTT(A,-1);
//		for(int i=0;i<(lim>>1);i++)printf("%d ",a[i]);printf("*");
//		for(int i=0;i<(lim>>1);i++)printf("%d ",b[i]);printf("=");
		for(int i=0;i<lim;i++)c[i]=A[i];
//		for(int i=0;i<lim;i++)printf("%d ",c[i]);puts("");
	}
	void inv(int*a,int*b,int LG){
		b[0]=ksm(a[0]);
		for(int k=1;k<=LG+1;k++){
			mul(b,a,C,k);
			for(int i=0;i<(1<<k);i++)C[i]=(mod-C[i])%mod;
			(C[0]+=2)%=mod;
			mul(C,b,b,k);
		}
	}
	void diff(int*a,int*b,int len){for(int i=0;i<len;i++)b[i]=1ll*a[i+1]*(i+1)%mod;b[len-1]=0;}
	void inte(int*a,int*b,int len){for(int i=len-1;i;i--)b[i]=1ll*a[i-1]*ksm(i)%mod;b[0]=0;}
	void ln(int*a,int*b,int LG){
		inv(a,b,LG);
		diff(a,C,1<<LG);
		mul(b,C,b,LG+1);
		inte(b,b,1<<LG);
	}
	void exp(int*a,int*b,int LG){
		b[0]=1;
		for(int k=1;k<=LG+1;k++){
			ln(b,D,k-1);
			for(int i=0;i<(1<<(k-1));i++)D[i]=(a[i]-D[i]+mod)%mod;
			(++D[0])%=mod;
			mul(b,D,b,k);
		}
	}
}using namespace Poly;
int a[N];
int f[N<<1],g[N<<1];
void solve(int l,int LG){
	if(!LG){f[2*l]=1,f[2*l+1]=(mod-a[l])%mod;return;}
	solve(l,LG-1),solve(l+(1<<(LG-1)),LG-1);
	mul(f+2*l,f+2*l+(1<<LG),f+2*l,LG+1);
}
int n,m,LG;
void calc(){
solve(0,LG);
//	for(int i=0;i<=n;i++)printf("%d ",f[i]);puts("");
	inv(f,g,LG);
	for(int i=0;i<=n;i++)f[i]=1ll*f[i]*(n-i)%mod;
	mul(f,g,f,LG+1);
//	for(int i=0;i<=n;i++)printf("%d ",f[i]);puts("");
}
void clac(int *p){for(int i=0;i<=n;i++)p[i]=1ll*p[i]*f[i]%mod;}
int fac[N],INV[N],p[N],q[N],r[N];
int main(){
	scanf("%d%d",&n,&m);
	while((1<<LG)<=n)LG++;
	for(int i=0;i<n;i++)scanf("%d",&a[i]);
	calc();
	fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
	INV[n]=ksm(fac[n]);for(int i=n;i;i--)INV[i-1]=1ll*INV[i]*i%mod;
	for(int i=0;i<=n;i++)p[i]=1ll*ksm(i+1,m)*INV[i]%mod;
	for(int i=0;i<=n;i++)q[i]=1ll*ksm(i+1,m<<1)*INV[i]%mod;
	inv(p,r,LG),mul(q,r,q,LG+1),clac(q);
	ln(p,r,LG),clac(r),exp(r,p,LG);
	mul(p,q,r,LG+1);
	int res=1ll*r[n-2]*fac[n-2]%mod;
	for(int i=0;i<n;i++)res=1ll*res*a[i]%mod;
	printf("%d\n",res);
	return 0;
}
posted @   Troverld  阅读(196)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示