CF842E Nikita and game
CF842E Nikita and game
题意简述:每次加一个点,求加点后树的直径的端点数。
数据范围:\(N\le 3\times 10^5\)。
Solution
维护直径的"两部",开两个std::vector保存它们。
每次新加一个点,考虑对直径的变化。由于直径的最长性,直径每次只会加\(1\)。
我们考虑每次对维护的std::vector进行操作,如果以它为一端的直径长度和之前相等,我们将它加入一个std::vector。如果以它为一端的直径长度大于答案,我们将一个std::vector清空。
我们发现每个点最多只会加入一次,删除数显然小于加入数,所以我们暴力维护就行了。
现在我们只需找到每个点所对应的最长路径。如果我们枚举直径的另一端,每次找到直径的时间复杂度是\(O(N log N)\),总的时间复杂度是\(O(N^2log N)\)。
- 我们考虑直径的性质。所有的直径都是相交的。
我们用反证法证明:在一张边权非负的树上,如果有两条不相交的直径,由于树上任意两点一定是连通的,我们交换两条直径的一端,一定能找到一个更大的直径。这与直径的最大性相悖。所以所有的直径都是相交的。
- 所以两个点集构成的路径上一定有重合的地方,我们称它为关键点。
容易发现,在一个点集内的任意两点到关键点的距离相等。
一个点如果想要更新答案,那么他一定加入到了一个点集(不管其他的点有没有清空)。
如果它是一个新的答案,那么直径一定是过关键点的。否则它就不可能是直径(没有与其他直径都相交)。
所以如果这个点能更新答案,那么它到点集内任意一个点的距离都是一样长的,我们只需\(O(log N)\)的用一个点集内第一个点进行转移判断就行了,总的时间复杂度为\(O(Nlog N)\)。
代码如下:
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=3e5+10;
int n,f[N][30],d[N],cnt;
vector<ll>S,T;
ll D;
void add(int x,int fa){
f[x][0]=fa,d[x]=d[fa]+1;
for(int i=1;(1<<i)<=d[x];i++)
f[x][i]=f[f[x][i-1]][i-1];
}
int lca(int x,int y){
if(d[x]<d[y])swap(x,y);
for(int i=25;i>=0;i--)if(d[f[x][i]]>=d[y])x=f[x][i];
if(x==y)return x;
for(int i=25;i>=0;i--)if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
ll dist(ll x,ll y){
return d[x]+d[y]-2*d[lca(x,y)];
}
int main(){
scanf("%d",&n);
d[1]=0,cnt=1;
S.push_back(1);
while(n--){
cnt++;
int x;scanf("%d",&x);add(cnt,x);
ll dis1=0,dis2=0;
if(S.size())dis1=max(dis1,dist(cnt,S[0]));
if(T.size())dis2=max(dis2,dist(cnt,T[0]));
if(dis1>D||dis2>D){
D=max(dis1,dis2);
if(D==dis1){
for(auto y:T)if(dist(cnt,y)==D)S.push_back(y);
T.clear();
T.push_back(cnt);
}else if(D==dis2){
for(auto y:S)if(dist(cnt,y)==D)T.push_back(y);
S.clear();
S.push_back(cnt);
}
}else{
if(dis1==D)T.push_back(cnt);
else if(dis2==D)S.push_back(cnt);
}
printf("%lld\n",S.size()+T.size());
}
return 0;
}

浙公网安备 33010602011771号