1. 程式人生 > 其它 >Prufer序列

Prufer序列

就是Prufer。。。

\(Prufer\)

既然提到樹,那就必定會提到 \(Prufer\) 序列。
\(Prufer\) 序列是為了證明 \(Cayley\) 公式而被髮明出來的,即一個 \(n\) 個點的完全圖中共有 \(n^{n-2}\) 個不同的樹,然而卻擁有更加強大的功能。
\(Prufer\) 序列可以將一棵 \(n\) 個點的樹唯一對映到一個長度為 \(n-2\) 的序列上,可以理解為,兩棵樹不相同,當且僅當它們的 \(Prufer\) 序列不同。

如何構造一棵樹的 \(Prufer\) 序列?
每次找到編號最小的一個葉子節點,將與其連線的那個點加入序列,並將其在樹上刪除,直到樹上只有兩個節點。

\(oi\)

-\(wiki\) 上盜一張圖,方便理解。

顯然 \(Prufer\) 序列會有一下兩個性質。
1、樹中最後剩下的兩個點中,一定會有一個節點是編號最大的 \(n\) 點;
2、每個節點在序列中出現的次數為這個點的度數-1。

思考為什麼 \(Prufer\) 序列要這樣構造?為什麼它可以唯一表示一棵樹?
嘗試證明:
不難發現,由於 \(Prufer\) 序列的構造方法十分巧妙,也就滿足許多特殊的性質。
在構造序列時,每次將與編號最小的葉子節點加入到序列中,而編號最小的葉子節點是很容易根據 \(Prufer\) 序列求出的,因此序列中的每個數表示的不是一個點而是一條邊,這樣就有了 \(n-2\)

條邊,而最後一條邊顯然是與 \(n\) 號節點相連的。以上,\(Prufer\) 序列就表示出了 \(n-1\) 條邊,也就唯一表示出了一棵樹。

接下來考慮如何構造一棵樹的 \(Prufer\) 序列,以及如何通過 \(Prufer\) 序列重構一棵樹。
Luogu P6086 【模板】Prufer 序列
存在優秀的線性做法。
令指標 \(p\) 指向最小的葉子節點,將其刪除,若出現了一個新的葉子節點,且比 \(p\) 小,那就繼續刪除,直至新出現的葉子節點比 \(p\) 大或沒有新的葉子節點。
\(p\) 指向新的最小葉子節點,並重覆上述操作。
重構樹的方法與構造 \(Prufer\) 序列類似,相信聰明的你一定能想出來。實際上就是我懶的寫了。。。

code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e6+10;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int n, m;
vector<int> l[N];
int fa[N], p[N], deg[N], tot;
ll ans;
inline void del(int x, int y) { p[++tot]=fa[x]; if(--deg[fa[x]]==1&&fa[x]<y) del(fa[x], y); }
inline void add(int x, int y) { fa[x]=p[++tot]; if(--deg[fa[x]]==0&&fa[x]<y) add(fa[x], y); }
int main(void){
	n=read(), m=read(); p[n-1]=n;
	if(m==1){
		for(int i=1; i<n; ++i) fa[i]=read();
		for(int i=1; i<n; ++i) ++deg[i], ++deg[fa[i]];
		for(int i=1; i<n; ++i) if(deg[i]==1) del(i, i);
		for(int i=1; i<n-1; ++i) ans^=1ll*i*p[i];
	}else{
		for(int i=1; i<n-1; ++i) p[i]=read(), ++deg[p[i]];
		for(int i=1; i<n; ++i) if(!deg[i]) add(i, i);
		for(int i=1; i<n; ++i) ans^=1ll*i*fa[i];
	}
	printf("%lld\n", ans);
	return 0;
}