zoukankan      html  css  js  c++  java
  • Prufer 序列小记

    前言

    因为 这道题 滚过来学 Prufer 了 /kel

    Prufer序列

    Prufer 序列是无根树的一种数列,可以将一个带标号的 (n) 点无根树用 ([1,n]) 中的 (n-2) 个整数表示,即 (n) 点完全图的生成树与长度为 (n-2) 值域为 ([1,n]) 的数列构成的双射。

    一般用来解决一类树相关的计数问题。

    从树到 Prufer

    构造方式:

    1. 每次选择一个编号最小的叶节点并删掉它,然后在序列中记录下它连接的那个节点。

    2. 重复这个过程直到剩下两个节点。

    直接做的时间复杂度是 (mathcal{O}(nlog n)) . 更优秀的做法:

    指针指向编号最小的叶节点,如果删掉之后产生了新的节点且比指向的更小,那么就直接删。否则指针找到下一个编号最小的叶节点。

    从 Prufer 到树

    根据 Prufer序列 的性质,可以得到原树上每个点的度数。

    一个度数为 (d[i]) 的点,在序列中出现的次数为 (d[i]-1) ,因为每次删去一个子节点都会在序列里面加入一次。

    具体构造方式如下:

    1. 初始时有一个 (1sim n) 的序列和一个 (n-2) 个元素的 Prufer 序列。
    2. 选择一个编号最小的度数为 (1) 的节点(也就是不在序列中的最小节点),与当前枚举到的 Prufer 序列的点相连,并在两个序列中分别删去这两个点。(显然,根据Prufer序列的构造过程逆推可得)
    3. (n-2) 次之后剩下两个度数为 (1) 的点,将它们相连即可。

    用类似的方式能够优化到 (mathcal{O}(n)) .

    代码实现

    【模板】Prufer 序列

    //Author: RingweEH
    const int N=5e6+10;
    int n,opt,deg[N],fa[N],prufer[N];
    ll ans=0;
    
    void TreeToPrufer()
    {
    	memset( deg,0,sizeof(deg) );
    	for ( int i=1; i<n; i++ )
    		fa[i]=read(),deg[fa[i]]++;
    	for ( int i=1,pos=1; i<=n-2; i++ )
    	{
    		while ( deg[pos] ) pos++;		//找一个编号最小的叶节点
    		prufer[i]=fa[pos];		//将其父亲加入序列
    		for ( ; i<=n-2; )
    		{
    			deg[prufer[i]]--;
    			if ( deg[prufer[i]] ) break;	//减完之后不是叶
    			if ( prufer[i]>=pos ) break;	//是叶节点但是编号在后面
    			prufer[i+1]=fa[prufer[i]]; i++; 	//新产生的点加入序列
    		}
    		pos++;
    	}
    	for ( int i=1; i<=n-2; i++ )
    		ans=ans^(1ll*i*prufer[i]);
    	printf( "%lld
    ",ans );
    }
    
    void PruferToTree()
    {
    	memset( deg,0,sizeof(deg) );
    	for ( int i=1; i<=n-2; i++ )
    		prufer[i]=read(),deg[prufer[i]]++;
    	prufer[n-1]=n;
    	for ( int i=1,pos=1; i<n; i++ )
    	{
    		while ( deg[pos] ) pos++; 	//自增找一个叶节点
    		fa[pos]=prufer[i];	//与当前序列首相连
    		for ( ; i<n-1; )
    		{
    			deg[prufer[i]]--;
    			if ( deg[prufer[i]] ) break;
    			if ( prufer[i]>=pos ) break;
    			fa[prufer[i]]=prufer[i+1]; i++;
    		}
    		pos++;
    	}
    	for ( int i=1; i<n; i++ )
    		ans=ans^(1ll*i*fa[i]);
    	printf( "%lld
    ",ans );
    }
    
    int main()
    {
    	n=read(); opt=read();
    	if ( opt==1 ) TreeToPrufer();
    	else PruferToTree();
    
        return 0;
    }
    

    凯莱公式(Cayley's Formula)

    由于 (n) 点的 Prufer序列 有 (n^{n-2}) 种,根据双射的关系,有结论:

    (n) 点带标号无根树有 (n^{n-2}) 种。

    拓展:

    设树上点 (i) 的度数为 (d_i) ,对应无根树数量为 (dfrac{(n-2)!}{prod_{i=1}^n (d_i-1)!}) .(其实就是可重元素全排列,因为是双射嘛)

    Generalized Cayley's Formula

    结论:设 (f(n,m))(n) 个点组成 (m) 棵树,且 (1sim m) 不在同一棵树中的方案数(有标号,无根),有

    [f(n,m)=mn^{n-m-1} ]

    Cayley's Formula 是 (m=1) 的特例。

    证明看这里

    拓展

    (n) 个带权点,边权为两两点权之积,树的权值为边权之积。对于所有 (n) 点带标号无根树,其树权总和为:

    [left(prod_{i=1}^n{val_i} ight)left(sum_{i=1}^nval_i ight)^{n-2} ]

    习题

    树的计数

    一个有 (n) 个节点的树,设它的节点分别为 (v_1,v_2,ldots,v_n) ,已知第 (i) 个节点 (v_i) 的度数为 (d_i) ,问满足这样的条件的不同的树有多少棵。

    Solution

    直接套 (dfrac{(n-2)!}{prod_{i=1}^n (d_i-1)!}) 的公式。但是需要高精/质因数分解。

    得知了某个奇怪的技巧:可以缩小选择范围,设之前已经枚举的 (d_i-1) 之和为 (sum) ,那么对于新的 (d_i-1) ,方案数只有 (C(n-2-sum,d_i-1)) 种,这样就不需要在计算上面做更多的处理,预处理组合数即可。

    然后注意一些特判:

    • (n=1) 的时候判一下度数
    • (sum d_i=2*n-2) ,所以 (sum d_i-1=n-2) ,如果不相等说明不连通,如果中间有度数为 (0) 也是。
    //Author: RingweEH
    const int N=160;
    int n,d[N];
    ll C[N][N];
    
    void C_init()
    {
    	for ( int i=0; i<=n; i++ )
    	{
    		C[i][0]=1;
    		for ( int j=1; j<=i; j++ )
    			C[i][j]=C[i-1][j]+C[i-1][j-1];
    	}
    }
    
    int main()
    {
    	n=read();
    	if ( n==1 )
    	{
    		d[1]=read();
    		if ( d[1]==0 ) printf( "1" );
    		else printf( "0" );
    		return 0;
    	}
    
    	C_init(); int sum=0;
    	for ( int i=1; i<=n; i++ )
    	{
    		d[i]=read();
    		if ( !d[i] ) { printf( "0" ); return 0; }
    		d[i]--; sum+=d[i];
    	}
    	if ( sum^(n-2) ) { printf( "0" ); return 0; }
    	sum=0; ll ans=1;
    	for ( int i=1; i<=n; i++ )
    		ans=ans*C[n-2-sum][d[i]],sum+=d[i];
    
    	printf( "%lld
    ",ans );
    
        return 0;
    }
    

    明明的烦恼

    给出标号为 (1sim n) 的点,以及最后某些(没有限定哪些)点的度数,任意连边,问有多少符合要求的树。

    Solution

    记没有限定的位置数量为 (k) .

    根据Prufer序列,令 (S=sum d_i-1) ,显然有:

    [dfrac{(n-2)!}{prod_{i=1}^n (d_i-1)!} ]

    由于这 (S) 个位置任意选择,要乘上 (C_{n-2}^S) ;而剩下了 (n-2-S) 个位置,可以任意排布,所以还有 ((n-k)^{n-2-S}) .

    [C_{n-2}^Scdotfrac{S!}{prod_{i=1}^k(d_i-1)!}*(n-k)^{n-2-s} ]

    高精度就咕咕咕了

    另一个重要结论

    CF156D Clues 给定 (n)(m) 边的带标号无向图,有 (k) 个连通块,求加 (k-1) 条边使得图连通的方案数。


    把每个连通块看成一个点,用 Prufer 计数,方案数为

    [sumdfrac{(k-2)!}{prod_{i=1}^k(d_i-1)!},d_ige 1,sum d_i=2k-2 ]

    然后每个连通块内要选出一个点作端点,设 (siz[i]) 表示第 (i) 个连通块大小,答案为(省略条件)

    [sumdfrac{(k-2)!}{prod_{i=1}^k(d_i-1)!}prod_{i=1}^ksiz[i]^{d_i} ]

    (b_i=d_i-1) ,用多元二项式定理带入得到

    [n^{k-2}prod_{i=1}^ksiz[i] ]

    //Author: RingweEH
    int main()
    {
    	n=read(); m=read(); P=read();
    	if ( P==1 ) { puts("0"); return 0; }
    	for ( int i=1; i<=n; i++ ) fa[i]=i;
    	for ( int i=1,u,v; i<=m; i++ ) 
    		u=read(),v=read(),u=find(u),v=find(v),fa[u]=v;
    	for ( int i=1; i<=n; i++ ) 
    		++siz[find(i)];
    	for ( int i=1; i<=n; i++ )
    		if ( i==find(i) ) k++,(ans*=siz[i])%=P;
    	if ( k==1 ) { puts("1"); return 0; }
    	ans=ans*power(n,k-2)%P;
    	printf( "%lld
    ",ans );
    
    	return 0;
    }
    

    学习材料

    Prufer codes与Generalized Cayley's Formula学习笔记

    xht37的模板题题解

    天光渐亮。
  • 相关阅读:
    .bat文件打开指定网页,并运行jar包
    jar包制作一个可执行文件
    如何让局域网其他电脑通过IP直接访问自己电脑的网站
    Sypder 安装和使用
    tomcat服务器输入localhost可以访问,ip无法访问解决办法
    【转载】高性能网站建设
    网站优化
    JavaWeb 项目开发中的技术总结
    反射工具类——ReflectUtils
    Ajax 的缺点
  • 原文地址:https://www.cnblogs.com/UntitledCpp/p/Prufer.html
Copyright © 2011-2022 走看看