树的直径新求法
讲解题目
今天考了一道题目,下面的思路二是我在考场上原创,好像没人想到这种作法,最原始的题目,考场上的题目是这样的:算法
你如今有1 个节点,他的标号为1,每次加入一个节点,第i 次加入的节点标号为i+1,且每次加入的节点父亲为已经存在的节点。你须要在每次加入节点后输出当前树的直径。数组
大意:给你$n$个点,最开始时只有一个点,一号节点,而后每一次把第$i$号节点和编号比他小的节点相连,链接一个点后,求树的直径。spa
样例输入
第一行一个整数n。接下来n-1 行,第i+1 行一个数表示标号为i+1 的点的父亲。code
6 1 2 2 1 5
样例输出
一行n-1 个数,第i 个数表示加入i+1 号点后树的直径。blog
1 2 2 3 4
思路
先说一下常规的作法,咱们看一下题目,会发现一个性质,很好的性质。在每一次加点以后,只有三种状况产生,第一种就是几点以后没有什么用,树的直径仍是以前那个;第二种状况就是当前点和上一次的一个端点组成;而最后一种和第二种同样,只是是和另外一个端点。用字母表示一下就是:设当前直径的表示方法为$(x1,x2)$,表示的是以$x1,x2$为两个端点的链。设新加进来的点为$y$,则新产生的树的直径只有三种状况:$(x1,y)$、$(y,x2)$、$(x1,x2)$。只须要维护一个倍增lca 和每一个点的深度就行啦。这个不难想到(对与大佬们来讲,像我这样的蒟蒻就没想到)。get
虽然我没有想到,可是我自创了另外一种作法。咱们设$f[i]$表示的是以$i$号节点为根的子树中以$i$为端点的最长长度。这样咱们计算答案时就很好计算了,咱们只须要把新加进来的点旋转到整棵树的根,并更新$f$数组的值,把新加进来的点的$f$数组的值和上一个直径进行对比,去最大值就能够啦。难点就是旋转,你们还记的splay的旋转吗,这道题的旋转和splay的旋转的思路差很少,都是把儿子旋上去。可是相对而言这个更简单,由于咱们不须要存他的儿子有谁,只须要存父亲就行了,可是咱们发现旋转的时候旋下来的点的$f$数组的值会改变,因此咱们须要从新更新。怎么更新呢?咱们每一次都在和当前旋下来的点有一条边相连的全部点中选取$f$值最大的,并加一赋值,但有一个细节,就是这些点中不能包括要选上去的点。每一次更新就好啦。it
时间分析
可能有人会问,这个不是$O(N^2)$的时间复杂度吗?虽然是指望$log_2^n$层,可是数据卡一卡就能卡成链,退化成$N^2$的算法啦。可是不要忘记,咱们是有旋转操做的,旋来旋去,就成了一棵相似于平衡树的东西,由于你也不知道怎么旋,在没看代码的状况下,因此数据你作不出来,哈哈。再想想,splay的精妙之处不也是这里吗?由于旋转成了平衡树。结束,时间复杂的$O(n*log_2^n)$。是否是很好?io
代码
#include <stdio.h> #include <algorithm> using namespace std; #define N 200001 int head[N],to[N*2],nxt[N*2]; int ans,idx,n; int f[N]; int lenth[N]; void add(int a,int b) {nxt[++idx]=head[a],head[a]=idx,to[idx]=b;} void change(int p,int from) { if(f[p]) change(f[p],p); lenth[p]=0; for(int i=head[p];i;i=nxt[i]) if(to[i]!=from) lenth[p]=max(lenth[p],lenth[to[i]]+1); f[p]=from; } int main() { scanf("%d",&n); for(int i=2;i<=n;i++) { scanf("%d",&f[i]),add(f[i],i),add(i,f[i]); change(f[i],i),lenth[i]=lenth[f[i]]+1,f[i]=0; ans=max(ans,lenth[i]); printf("%d ",ans); } }
有不懂得,能够发评论提问,这种作法纯属原创。class