【算法竞赛学习笔记】Purfer序列

本文详细介绍了Prufer序列的性质及其在树的构造与计数中的应用,包括从序列构建树和从树构建序列的方法。此外,还讨论了完全图的生成树计数、图的连通方案数以及相关算法实现,如快速幂和基尔霍夫矩阵。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


title : Prufer
date : 2022-5-9
tags : ACM,计数,图论
author : Linno


Purfer序列

Prufer是一种将带符号的树用一个唯一的整数序列表示的方法。可以用来证明凯莱定理(Cayley’s formula:一个含有n个节点的完全图的生成树个数为 n n − 2 n^{n-2} nn2,即带有标号的n个结点的无根树的个数为 n n − 2 n^{n-2} nn2),也可以计算一个图加边使得图连通的方案。

性质

  • 在构造完Prufer序列后原树中会剩下两个结点,其中一个一定是编号最大的点n
  • 每个结点在序列中出现的次数是其度数-1(没有出现的就是叶子结点)
  • purfer序列与带编号无根树形成双射。
  • 对于给定度数d1~dn的一棵无根树,一共有 ( n − 2 ) ! ∏ i − 1 n ( d i − 1 ) ! , 其 中 s u m = n − 2 − ∑ j − 1 i − 1 ( d j − 1 ) \frac{(n-2)!}{\prod_{i-1}^n(d_i-1)!},其中sum=n-2-\sum_{j-1}^{i-1}(d_j-1) i1n(di1)!(n2)!,sum=n2j1i1(dj1)

对树建立Prufer序列

prufer

(图片转自OI-WIKI)

每次选择一个编号最小的叶子结点删除,如何在序列中记录下它连接到的那个结点,重复n-2次,就只剩下两个结点,算法结束。

堆O(nlogn)
vector<int>prufer(){
    set<int>leafs;
    for(int i=0;i<n;i++){
        deg[i]=adj[i].size();//adj为邻接矩阵
        if(deg[i]==1) leafs.insert(i);
    }
    vector<int>code(n-2);
    for(int i=0;i<n-2;i++){
        int leaf=*leafs.begin();
        leafs.erase(leafs.begin());
        kill[leaf]=1;
        int v;
        for(int u:adj[leaf]) if(!kill[u]) v=u;
        code[i]=v;
        if(--deg[v]==1) leafs.insert(v);
    }
    
}
线性构造

维护一个指针指向我们要删除的结点,一开始p指向编号最小的叶子节点。复杂度 O ( n ) O(n) O(n)操作过程如下:

  • 删除p指向的结点,并检查是否产生新的叶子结点。
  • 如果产生新的叶子结点x,我们比较p,x的大小关系。如果x>p,那么不做其他操作;否则立刻删除x,然后检查删除x后是否产生新的叶子结点,重复直到未产生新结点或者新结点的编号>p
  • 让指针p指针直到遇到一个未被删除叶子结点为止。
vector<vector<int>>G;
vector<int>fa;
void dfs(int x){
    for(auto to:G[x]){
        if(to!=fa[x]) fa[to]=x,dfs(to);
    }
}

vector<int>prufer(){
    int n=G.size();
    fa.resize(n);
    fa[n-1]=-1;
    dfs(n-1);
    int ptr=-1;
    vector<int>deg(n);
    for(int i=0;i<n;i++){
        deg[i]=G[i].size();
        if(deg[i]==1&&ptr==-1) ptr=i;//找到编号最小的叶子结点
    }
    vector<int>code(n-2);
    int leaf=ptr;
    for(int i=0;i<n-2;i++){
        int nxt=fa[leaf];
        code[i]=nxt;
        if(--deg[nxt]==1&&nxt<prt) leaf=nxt; //如果父亲编号更小,就是下一个要删除的对象
        else{
            ptr++;
            while(deg[ptr]!=1) ptr++; //自增到最小的叶子节点
            leaf=ptr;
        }
    }
    return code;
}

对Prufer序列构造树

我们由上述性质可以求出树上每个点的度数,每次我们选择一个编号最小的叶子节点,与当前枚举到的Prufer序列进行连边,然后同时减掉两边的度数,重复n-2次就只剩下两个度数为1的结点了,其中一个为根节点,连接即可。

显然既可以用堆,也可以线性构造,就是上述过程的逆过程。

【模板】Prufer 序列

很简单的一道模板,但是自己写的比较繁琐就搬了洛谷的置顶题解。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e6+7;
int n,o,f[N],p[N],d[N],ans;

inline void rd(int &data){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}data=f*x;}
inline void write(int x){if(x>9) write(x/10);putchar(x%10+'0');}

inline void TP(){
	for (int i = 1; i < n; i++) rd(f[i]), ++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;
	}
	for (int i = 1; i <= n - 2; i++) ans ^= 1ll * i * p[i];
}

inline void PT() {
	for (int i = 1; i <= n - 2; i++) rd(p[i]), ++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;
	}
	for (int i = 1; i < n; i++) ans ^= 1ll * i * f[i];
}

signed main() {
	rd(n), rd(o), o == 1 ? TP() : PT(), write(ans);
	return 0;
}

UVA10843 Anne’s game

给定一个n个结点的完全图,求生成树的个数。

Purfer题解

题目满足Prufer序列的特性:

①prufer序列与无根树一一对应

②prufer序列中某个编号出现的次数就等于这个编号的结点在无根树中的度数-1

③n个点的完全图所构成的生成树计数就是 n n − 2 n^{n-2} nn2

一个n个结点的树,它的prufer序列长度是n-2,每个位置都有n种选择,那么直接用快速幂就做完了。代码非常简短。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=2000000011ll;
 
int fpow(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=res*a%mod;
		a=a*a%mod;
		b/=2;
	}
	return res;
}

int n,x;

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>x;
		cout<<"Case #"<<i<<": "<<fpow(x,x-2)<<"\n";
	}
	return 0;
}
基尔霍夫矩阵

这个矩阵基本可以解决生成树计数问题,复杂度 O ( n 3 ) O(n^3) O(n3)(高斯消元)。原理在我MST的博客上有提到,构造方式很好记:K(基尔霍夫矩阵)=D(度数矩阵)-A(邻接表)

//#pragma GCC optimize("Ofast", "inline", "-ffast-math")
//#pragma GCC target("avx,sse2,sse3,sse4,mmx")
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=2000000011ll;

//int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}
//void write(int x){if(x>9) write(x/10);putchar(x%10+'0');}

int n,sq[105][105]; 

void Solve(int id){
	cin>>n;
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){
		if(i==j) sq[i][j]=n-1;
		else sq[i][j]=-1;
	}
	int res=1;
	for(int i=2;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			while(sq[j][i]){
				int t=sq[i][i]/sq[j][i];
				for(int k=i;k<=n;k++) sq[i][k]=(sq[i][k]-sq[j][k]*t);
				for(int k=i;k<=n;k++) swap(sq[i][k],sq[j][k]);
				res=-res;
			}
		}
		res*=sq[i][i];
		res%=mod;
	}
	if(res<0) res=-res;
	cout<<"Case #"<<id<<": "<<res<<"\n";
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int T;
	cin>>T;
	for(int t=1;t<=T;t++){
		Solve(t);
	}
	return 0;
}

图连通方案数

给定一个n个点m条边的带标号无向图有k个连通块,求添加k-1条边使得整个图连通的方案数

如果是对已经有部分连通的图进行计数,恐怕没有比purfer序列更好处理的方法了。我们假设 s i s_i si为每个连通块的大小,尝试对 k k k个连通块构造Prufer后,最终答案可以由结论得出: a n s = n k − 2 ∏ i = 1 k s i ans=n^{k-2}\prod_{i=1}^ks_i ans=nk2i=1ksi(证明略)

树的计数

给定n个结点的树以及每个结点的度数,求满足条件的树有多少棵。

这道题正可以用于对最后一条性质的解释,考虑一个组合数学问题,度数为 d i d_i di的结点会在prufer序列中出现 d i − 1 d_i-1 di1次,那么题目就转化为了求 d i − 1 个 i d_i-1个i di1i的全排列个数。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=200;

int n,C[N][N],deg[N];

void init(int m){
	for(int i=0;i<=m;i++){
		C[i][0]=C[i][i]=1;
		for(int j=1;j<=i;j++) C[i][j]=C[i-1][j]+C[i-1][j-1];
	}
}

signed main(){
	cin>>n;
	init(n);
	int sum=0,ans=1;
	for(int i=1;i<=n;i++) cin>>deg[i],sum+=deg[i]-1;
	if(n==1) cout<<(deg[1]^1ll)<<"\n";
	else if(sum!=n-2) cout<<"0\n";
	else{
		for(int i=1;i<=n;i++){
			if(!deg[i]){
				cout<<"0\n";
				return 0;
			}
			ans=ans*C[sum][deg[i]-1];
			sum-=deg[i]-1;
		}
		cout<<ans<<"\n";
	}
	return 0;
}

参考资料

https://oi-wiki.org/graph/prufer/

https://www.luogu.com.cn/blog/xht37/solution-p6086

https://blog.youkuaiyun.com/qq_35802619/article/details/108342700

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RWLinno

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值