Prufer 序列
大概是因为最近学多项式时候常碰到用 Prufer 序列数树的题就来学了。
Prufer 序列,又称 Purfer 序列、Prufur 序列、Prefer 序列,支持将一棵无根树压缩成一个长度为 \(n-2\) 的、每个数都在 \([1,n]\) 范围内的序列,当然它也支持解压操作。
那么如何对树构造出它的 Prufer 序列呢?每次选择编号最小的叶节点并删掉它,然后在序列中记录下它连接到的那个节点,这样操作 \(n-2\) 次就可以得到原树的长度为 \(n-2\) 的 Prufer 序列。上面的模拟过程显然可以用堆实现,复杂度线对。当然稍微想想也可得到线性的做法:用一个指针维护当前所有叶子节点中编号最小的那一个,然后每做一次操作后,判断被操作的节点是否为叶子节点并且比当前指针还要小,若是则采用新值即可。
const int MAXN=5e6;
int n,tp,f[MAXN+5],p[MAXN+5],deg[MAXN+5];
void solve1(){
for(int i=1;i<=n-1;i++) scanf("%d",&f[i]),deg[f[i]]++;
int cur=1;
for(int i=0;i<=n-2;){
while(deg[cur]) cur++;p[++i]=f[cur];
while(!--deg[p[i]]&&p[i]<cur) p[i+1]=f[p[i]],i++;
cur++;
} //for(int i=1;i<=n-2;i++) printf("%d\n",p[i]);
ll ans=0;for(int i=1;i<=n-2;i++) ans^=1ll*i*p[i];
printf("%lld\n",ans);
}
将 Prufer 序列还原成原来的无根树也是同样道理。根据上面的构造过程我们知道,一个节点在 Prufer 序列中出现的次数等于它的度数减 \(1\)。于是可以根据 Prufer 序列直接求出每个点的度。还原的时候考虑从左往右遍历整个 Prufer 序列,假设当前点为 \(p_i\),我们找出编号最小的叶子节点 \(x\),并将 \(x\) 与 \(p_i\) 相连,将 \(p_i\) 的度减去 \(1\)。如此操作 \(n-2\) 次就行了。上面的模拟过程还是显然可以用堆实现,复杂度线对。按照树转 Prufer 优化的套路也可优化到线性。
void solve2(){
for(int i=1;i<=n-2;i++) scanf("%d",&p[i]),deg[p[i]]++;
int cur=1;
for(int i=0;i<=n-2;){
while(deg[cur]) cur++;f[cur]=p[++i];
while(!--deg[p[i]]&&p[i]<cur) f[p[i]]=p[i+1],i++;
cur++;
} f[p[n-2]]=n;//for(int i=1;i<=n-1;i++) printf("%d\n",f[i]);
ll ans=0;for(int i=1;i<=n-1;i++) ans^=1ll*i*f[i];
printf("%lld\n",ans);
}
最后稍微讲讲 Prufer 序列的性质:
- 一个数在 Prufer 序列里的出现次数是其度数减一,因为每个点都有成为叶子节点的时候,这时我们直接把它删去,使其度数减 \(1\) 不会记录到 Prufer 序列中,而其余时刻将其度数减 \(1\) 都会使其在 Prufer 序列中的出现次数多 \(1\),故 \(u\) 在 Prufer 序列里的出现次数为 \(deg_u-1\)。
- 最后剩余的两个点中一定有一个点编号为 \(n\)——因为无论如何树中一定会存在至少两个叶子节点,而 \(n\) 永远不会成为编号较小的那一个,故 \(n\) 永远不会被删去。
- 由 \(n\) 个点组成的无根树个数为 \(n^{n-2}\),这同时也说明了由 \(n\) 个点组成的无根树与长度为 \(n-2\),值域为 \([1,n]\) 的序列构成了双射关系(至于为什么双射我也不太清楚)