Prufer 序列

hekaiyu

2020-05-23 23:17:19

Solution

# Prufer 序列 Prufer 序列是无根树的一种序列。对于一个 $n$ 个结点的树,Prufer 序列的长度为 $n-2$ 个,会用于解决一些有关树的计数问题。 先讲一讲 Prufer 序列是怎么得到的吧。 [P6086 【模板】Prufer 序列](https://www.luogu.com.cn/problem/P6086) 1. 每次找到树中入度为 $1$ 且编号最小的点。 2. 删去该节点并在序列中添加与该节点相邻的结点编号。 3. 重复上面的操作,知道剩下两个节点。 看张图,了解一下 Prufer 序列的生成过程,(样例一为例)。 ![prufer](https://cdn.luogu.com.cn/upload/image_hosting/ybdj7lav.png) ## 朴素算法 - 建图 - 暴力枚举寻找入度为 $1$ 且编号最小的点。 - 添加与该节点相邻的结点编号 ``` long long make_prufer(int n,int *d,int *p){ for(int i=1;i<n;++i){ int a;a=read(); cc(i,a);cc(a,i);//建图 ++d[a];++d[i]; } int head=0; for(int i=1;i<=n-2;++i){ while(d[++head]!=1);//寻找节点 for(int j=ves[head];j;j=st[j].next){ if(!d[st[j].v])continue; p[i]=st[j].v; --d[st[j].v]; --d[head];//处理完后删点 if(d[st[j].v]==1&&head>st[j].v)head=st[j].v-1;//发现更小结点跳回去 } } long long ans=0; for(int i=1;i<=n-2;++i)ans=(long long)((long long)i*p[i])^ans;//答案统计 return ans; } //转树的方法类似 long long make_fa(int n,int *d,int *fa){ for(int i=1;i<=n-2;i++){ prufer[i]=read(); ++d[prufer[i]]; } prufer[n-1]=n; int head=1,top=n-2; for(int i=1;i<=n-1;++i){ while(d[head]!=0)++head;//寻找最小结点 fa[head]=prufer[i]; --d[prufer[i]]; --d[head]; if(d[prufer[i]]==0&&prufer[i]<head)head=prufer[i]; } long long ans=0; for(int i=1;i<=n-1;++i)ans=(long long)((long long)i*fa[i])^ans; return ans; } ``` 显然上述的方法时间复杂度 $O(n^2)$ 。预计得分 $20$ 分(实际 $60$ 分)。 虽然办法很傻但起码让人了解了定义。 ## 堆优化 显然上面的代码一步一步寻找结点的效率很低。其实只需要将每次操作后入度为一的点扔到兑礼即可 时间复杂度 $O(n \log n)$,预期得分 $60$ 分。 代码类似就不贴了。 ## 再优化----时间复杂度 $O(n)$ 考虑到一棵树最少有两个叶子结点,所以点数为 $n$ 的点一定会留到最后,题目中恰好以 $n$ 为根节点,所以根本没必要存图。 好了还是开始将本题的正解吧。 我们使用指针来标记处理到目前为止叶子节点的最小编号,我们仍然像朴素算法一样自增查找,但在删除节点后,我们并没有必要跳回去啊。新产生的叶子节点无非存在两种情况: 1. 比指针小:由于指针是指向操作前的最小结点,比指针小说明它肯定是目前最小的叶子节点。直接进行处理即可 2. 比指针大:有可能不是目前最小的,不影响指针的正常操作,继续自增寻找即可。 ``` #include<bits/stdc++.h> using namespace std; const int maxn=5000005; int read(){//快读 int res,f=1; char c; while((c=getchar())<'0'||c>'9') if(c=='-')f=-1; res=c-48; while((c=getchar())>='0'&&c<='9') res=res*10+c-48; return res*f; } int n,type; int f[maxn],d[maxn],topss[maxn],x[maxn],top,maxv[maxn],p[maxn]; long long make_prufer(int n){ int headss=n;long long ans=0; for(int i=1;i<n;++i)f[i]=read(),++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;//代替朴素算法中的if } for(int i=1;i<=n-2;++i)ans^=(long long)i*p[i]; return ans; } long long make_fa(int n){ long long ans=0; for(int i=1;i<=n-2;++i)p[i]=read(),++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^=(long long)i*f[i]; return ans; } int main(){ n=read(),type=read(); if(type==1)printf("%lld",make_prufer(n)); if(type==2)printf("%lld",make_fa(n)); return 0; } ``` 提供一种倒序求解 fa 的方法(掠过吧),直接注释在代码里好了。 ``` long long make_fa(int n){ for(int i=1;i<=n-2;++i)p[i]=read(),++d[p[i]]; int head=n-2,top=n;long long ans=(long long)n*p[n-2];//特殊处理开头两个点 x[p[n-2]]=1,x[n]=1;//x表示是否已经处理 while(head){ if(d[p[head-1]]&&!x[p[head-1]]){//枚举prufer[head],如果该点没有在树里,那么在这个位置必须添加。 x[p[head-1]]=1; ans=(long long)((long long)p[head-1]*p[head])^ans;//由prufer[head-1]指向prufer[head] --head; continue; } while(x[top]&&top)--top;//当前处理到的点 x[top]=1; //之前是去掉最小的结点,所以贪心去寻找最小结点与prufer[head]连边。 ans=(long long)((long long)top*p[head])^ans; --head;//枚举下一个点 } return ans; } ``` 时间复杂度也是 $O(n)$ (常数会大一点点吧)。 ## prufer 序列的一些性质 1. 与一棵有标号的无根树对应 1. 某编号结点的入度为该点在prufer 序列中出现的次数 $+1$ 1. 点数为 $n$ 的有标号无根树个数为 $n^{n-2}$,有根树当然就 $\times n$ 就行啦。 1. 每个点度数为 $d_i$ 的有标号无根树个数为 $\frac{(n-2)!}{\prod\limits_{i=1}^n(d_i-1)!}$ 例题: [P2290 [HNOI2004]树的计数](https://www.luogu.com.cn/problem/P2290)