【数学】prufer 序列

发布时间 2023-11-20 20:17:36作者: The_Last_Candy

题目描述

请实现 Prüfer 序列和无根树的相互转化。

为方便你实现代码,尽管是无根树,我们在读入时仍将 \(n\) 设为其根。

对于一棵无根树,设 \(f_{1\dots n-1}\) 为其父亲序列\(f_i\) 表示 \(i\)\(n\) 为根时的父亲),设 \(p_{1 \dots n-2}\) 为其 Prüfer 序列

另外,对于一个长度为 \(m\) 的序列 \(a_{1 \dots m}\),我们设其权值\(\operatorname{xor}_{i = 1}^m i \times a_i\)

\(1 \leq n \leq 5 \times 10^6\)

算法描述

prufer 序列,用于将一个 \(n\) 个点的有标号无根树映射到一个长为 \(n - 2\) 的序列上。下面讲解实现过程。

无根树转化为 prufer 序列

考虑每次在叶节点当中选择编号最小的那一个,删除它和它连的边,在 prufer 序列中加入它指向的那个点,直到只有两个点为止。

我们发现,对于任意一棵无根树,一定可以转化为一个序列(树总有叶节点),而且过程当中每一步都是唯一的,所以树可以映射到序列上。

这样做用堆维护是 \(\Theta(n \log n)\) 的。

事实上可以用指针简单的代替这一过程,只需要每次右移,如果不是叶节点就不管,如果是就删掉,观察删掉过后有没有可能产生新的叶节点,并且如果产生了,这个点编号是否小于当前的指针,如果是的话不移动指针,继续删这个点就好了。

这样就解决了 “编号最小” 的问题,由于每个点只会被删一次,所以是 \(\Theta(n)\) 的。

inline void Tree_to_prufer()
{
	for(int i = 1;i <= n - 1;i++) deg[fa[i]]++,deg[i]++;
	int cnt = 0;
	for(int tp = 1;tp <= n;tp++)
	{
		if(deg[tp] > 1) continue;
		int now = tp;
		do{
			p[++cnt] = fa[now];
			deg[fa[now]]--; deg[now]--;
			if(deg[fa[now]] == 1) now = fa[now];
			else now = n + 2; 
		}while(now < tp);
	}
}
prufer 序列转化为无根树

我们研究上面的过程,第一步一定是选了 prufer 序列的第一个点和最小的叶节点连边。

prufer 序列一个显然的性质就是无根树上 \(x\) 的度数等于它在 prufer 序列中出现的次数 \(+1\)

所以我们可以通过序列得到点的度数,就可以知道第一步时,谁是最小的叶节点,这个点也是唯一的。

所以我们直接维护,连一条 \(p_1 \to leaf\) 的边,然后将两个度数都减 \(1\),类似上面的过程,每次找到编号最小的叶节点即可,优化方法和上面一样。

考虑结束以后,我们才连了 \(n - 2\) 条边,但是总共有 \(n - 1\) 条边。

我们发现 “编号最小的叶子” 这个事情怎么样也轮不到 \(n\) 。所以剩下的一定是 \(n\) 和另外一个点,连起来即可,也是 \(\Theta(n)\) 的。

inline void Prufer_to_tree()
{
	for(int i = 1;i <= n - 2;i++) deg[p[i]]++;
	for(int i = 1;i <= n;i++) deg[i]++;
	for(int i = 1,tp = 1;i <= n - 2;tp++) 
	{
		if(deg[tp] > 1) continue;
		int now = tp;
		do{
			fa[now] = p[i];
			deg[now]--; deg[p[i]]--;
			if(deg[p[i]] == 1) now = p[i];
			else now = n + 2;
			i++;
		}while(now < tp && i <= n - 2);
	}
	for(int i = 1;i < n;i++) if(deg[i] > 0) {fa[i] = n; break;}
}

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 5e6 + 5;
int deg[N],fa[N],p[N],n;
inline void Tree_to_prufer()
{
	for(int i = 1;i <= n - 1;i++) deg[fa[i]]++,deg[i]++;
	int cnt = 0;
	for(int tp = 1;tp <= n;tp++)
	{
		if(deg[tp] > 1) continue;
		int now = tp;
		do{
			p[++cnt] = fa[now];
			deg[fa[now]]--; deg[now]--;
			if(deg[fa[now]] == 1) now = fa[now];
			else now = n + 2; 
		}while(now < tp);
	}
}
inline void Prufer_to_tree()
{
	for(int i = 1;i <= n - 2;i++) deg[p[i]]++;
	for(int i = 1;i <= n;i++) deg[i]++;
	for(int i = 1,tp = 1;i <= n - 2;tp++) 
	{
		if(deg[tp] > 1) continue;
		int now = tp;
		do{
			fa[now] = p[i];
			deg[now]--; deg[p[i]]--;
			if(deg[p[i]] == 1) now = p[i];
			else now = n + 2;
			i++;
		}while(now < tp && i <= n - 2);
	}
	for(int i = 1;i < n;i++) if(deg[i] > 0) {fa[i] = n; break;}
}
int main()
{
	int op;
	scanf("%d%d",&n,&op);	
	if(op == 1) 
	{
		for(int i = 1;i <= n - 1;i++) scanf("%d",&fa[i]);
		Tree_to_prufer();
		long long ans = 0;
		for(int i = 1;i <= n - 2;i++) ans ^= 1ll * i * p[i];
		printf("%lld",ans);
	}
	else
	{
		for(int i = 1;i <= n - 2;i++) scanf("%d",&p[i]);
		Prufer_to_tree();
		long long ans = 0;
		for(int i = 1;i <= n - 1;i++) ans ^= 1ll * i * fa[i];
		printf("%lld",ans);
	}
	return 0;
 } 

prufer 序列的用法

著名的 Cayley 公式:

\(n\) 个点的有标号无根树的数量是 \(n^{n - 2}\)

考虑 prufer 序列的值域是 \([1,n]\) ,而且手模可以发现,任意一个长为 \(n - 2\) 的,值域 \([1,n]\) 的数列都可以转化成一个无根树,而且可以用上述方式映射回来,所以这是一个一一对应关系,prufer 序列的数量就是无根树的数量。

相关题目:

P6086 【模板】Prufer 序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

P4430 小猴打架 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

上面两道是板题,下面用另一道板题作为例题:

Clues - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

给定一个 \(n\) 个点 \(m\) 条边的带标号无向图,它有 \(k\) 个连通块,求添加 \(k-1\) 条边使得整个图连通的方案数,答案对 \(p\) 取模。

\(1 \leq n \leq 10^5,0 \leq m \leq 10^5,1 \leq p \leq 10^9\)

\(a_i\) 是第 \(i\) 个连通块的大小,考虑枚举每一棵树 \(T\) ,答案就是:

\[\sum_T \prod_{(u,v) \in T} a_ua_v \]

将按照边计数转化为按照点计数:

\[\sum_T \prod_{i = 1}^k a_i^{deg_i} \]

容易发现,一棵树的 prufer 序列每一项的 \(a\) 值相乘的结果是:

\[\prod_{i = 1}^k a_i^{deg_i - 1} \]

所以答案就是:

\[\sum_{s \in prufer} \prod_{i = 1}^{k - 2}a_{s_i} · \prod_{i = 1}^k a_i \]

所以我们要求所有的 \(k\)\(prufer\) 序列的 \(a\) 值乘积的和,假设 \(f_i\) 为前 \(i\) 项所有序列的和,不难发现最后一项填什么都可以,所以:

\[f_i = f_{i - 1} \times \sum_{i = 1}^ka_i \]

\[f_i = f_{i - 1} \times n \]

\[f_{k - 2} = n^{k - 2} \]

所以最终答案就是:

\[n^{k - 2} \times \prod_{i = 1}^k a_i \]

直接跑连通块 \(\Theta(n)\) 计算即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n,m,MOD;
struct Edge{
	int v,next;
}e[N * 2];
int vis[N],head[N],tot = 0,a[N],cnt = 0;
typedef long long ll;
ll f[N];
inline void add(int x,int y)
{
	++tot;
	e[tot].v = y;
	e[tot].next = head[x];
	head[x] = tot;
}
inline void dfs(int x,int num)
{
	++a[num];
	vis[x] = 1;
	for(int i = head[x];i;i = e[i].next)
	{
		int to = e[i].v;
		if(vis[to]) continue;
		dfs(to,num);
	}
}
inline ll ksm(ll base,int pts)
{
	ll ret = 1;
	for(;pts > 0;pts >>= 1,base = base * base % MOD)
		if(pts & 1)
			ret = ret * base % MOD;
	return ret;
 } 
int main()
{
	cin>>n>>m>>MOD;
	for(int i = 1,x,y;i <= m;i++)
	{
		cin>>x>>y;
		add(x,y);
		add(y,x);
	}
	if(MOD == 1) {puts("0"); return 0;}
	memset(vis,0,sizeof(vis));
	for(int i = 1;i <= n;i++) if(!vis[i]) {++cnt; dfs(i,cnt);}
	ll prod = 1;
	for(int i = 1;i <= cnt;i++) prod = prod * a[i] % MOD;
	if(cnt == 1) {puts("1"); return 0;}
	cout<<prod * ksm(n,cnt - 2) % MOD;
	return 0;
}