题目链接:点击查看
题目大意:给出一棵 n 个节点且以点 1 为根节点的的树,现在给出一个 k ,需要在树上选择 k 个关键点,使得 n 个点到达根节点的路径上,出现的最近的关键点的距离的最大值最小,现在需要输出 k 分别为 1 ~ n 时的答案之和
题目分析:首先理解题意,这个题目中有且仅由一个思维点需要转换过来,那就是直接计算 k 值下的答案是很难计算的,但是我们可以在某个答案下,贪心去计算 k 的值
怎么理解上面这段话呢?对于某个答案 x 来说,因为 x 是题目中所有距离最大值的最小值,换句话说,任何一个点到达根节点的路径上,相离最近的那个关键点的距离一定小于等于 x 的,这样的话我们可以贪心去模拟,假设 x 已经确定了,可以先找到当前深度最深的那个节点,显然如果想要让这个节点距离最近的关键点的距离小于等于 x 的话,那么令其向上的第 x 个祖先设置为关键点就好了,当刚才的那个祖先被设置为关键点后,其子树上的所有点也就都满足条件了(因为最深的那个点都满足条件了,那么其余的点显然也是满足条件的),依次类推,贪心去模拟就能计算出 k 的值了
现在我们需要选择合适的数据结构去实现上述操作,对于上段所述的功能一一对应一下,子树对应着dfs序,有了dfs序就可以建立线段树,这样每次找深度最深的结点就可以用线段树维护dfs序上深度的最大值,找向上第 x 个祖先,对应着树上倍增,然后就是枚举答案了,显然答案的可行范围是 0 ~ n - 1 (当所有点都是关键点时,答案为 0 ,当只有根节点是关键点,且整棵树退化为链时,那么答案就是 n - 1 ),对于每次枚举的答案为 i ,贪心需要模拟的次数就为 n / i ,整体的时间复杂度就是 (调和级数),再加上线段树更新需要的一层log,总的时间复杂度为 nlognlogn
注意一下,因为可能会出现不同的答案对应着相同的 k 值,所以我们可以倒着枚举答案,最后再正着维护一下答案的最小值就好了,因为根据贪心的策略,ans[ i ] 代表的就是当 k = i 时的答案,显然在 ans[ i ] 的策略上再增加关键点的话,ans[ i + 1 ] 一定是小于等于 ans[ i ] 的,所以从前往后维护最小值是可行的
最后就是对于线段树的更新,对于每个样例我们只需要 build 一次线段树即可,对于每个答案在更新线段树的时候,记录一下更新的位置,计算完答案后再原路恢复就好了,如果对于每个答案都重新 build 的话,时间复杂度就会退化成 n * n * logn * logn
代码:
#pragma comment(linker,"/STACK:102400000,102400000")
#include<iostream>
#include<cstdio>
#include<string>
#include<ctime>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<stack>
#include<climits>
#include<queue>
#include<map>
#include<set>
#include<sstream>
#include<cassert>
#include<bitset>
using namespace std;
typedef long long LL;
typedef unsigned long long ull;
const int inf=0x3f3f3f3f;
const int N=2e5+100;
int dp[N][25],ans[N],L[N],R[N],id[N],deep[N],cnt,limit,n;
vector<int>node[N];
void dfs(int u,int dep)
{
deep[u]=dep;
L[u]=++cnt;
id[cnt]=u;
for(int i=1;i<=limit;i++)
dp[u][i]=dp[dp[u][i-1]][i-1];
for(auto v:node[u])
dfs(v,dep+1);
R[u]=cnt;
}
struct Node
{
int id,deep;
bool operator<(const Node& t)const
{
if(deep!=t.deep)
return deep<t.deep;
return id<t.id;
}
}tree[N<<2],temp[N<<2];
int tot=0;
void build(int k,int l,int r)
{
if(l==r)
{
tree[k].deep=deep[id[l]];
tree[k].id=id[l];
temp[k]=tree[k];
return;
}
int mid=l+r>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
temp[k]=tree[k]=max(tree[k<<1],tree[k<<1|1]);
}
void update(int k,int l,int r,int L,int R,int op)//l,r:目标区间,L,R:当前区间
{
if(R<l||L>r)
return;
if(L>=l&&R<=r)
{
if(op==1)
tree[k].deep=-1;
else
tree[k].deep=temp[k].deep;
return;
}
int mid=L+R>>1;
update(k<<1,l,r,L,mid,op);
update(k<<1|1,l,r,mid+1,R,op);
tree[k]=max(tree[k<<1],tree[k<<1|1]);
}
int get_fa(int u,int x)
{
for(int i=0;i<=limit;i++)
if(x&(1<<i))
u=dp[u][i];
return max(u,1);
}
int cal(int x)
{
int num=0;
vector<int>cls;//恢复操作用
while(tree[1].deep!=-1)
{
num++;
int pos=get_fa(tree[1].id,x);
cls.push_back(pos);
update(1,L[pos],R[pos],1,n,1);
}
for(auto v:cls)
update(1,L[v],R[v],1,n,-1);
return num;
}
void init(int n)
{
limit=log2(n)+1;
cnt=0;
for(int i=0;i<=n;i++)
{
ans[i]=inf;
node[i].clear();
}
}
int main()
{
#ifndef ONLINE_JUDGE
// freopen("data.in.txt","r",stdin);
// freopen("data.out.txt","w",stdout);
#endif
// ios::sync_with_stdio(false);
while(scanf("%d",&n)!=EOF)
{
init(n);
for(int i=2;i<=n;i++)
{
scanf("%d",&dp[i][0]);
node[dp[i][0]].push_back(i);
}
dfs(1,0);
build(1,1,n);
for(int i=n;i>=0;i--)
{
int q=cal(i);
ans[q]=i;
}
for(int i=1;i<=n;i++)
ans[i]=min(ans[i],ans[i-1]);
LL ans=0;
for(int i=1;i<=n;i++)
ans+=::ans[i];
printf("%lld\n",ans);
}
return 0;
}