题解 CF208E 【Blood Cousins】

[ ext{关于本题} ]

(quad) (Dsu) (on) (Tree)模板题,没有做过的可以做做CF600E Lomsat gelral,也是一道模板题,下面就简单讲讲树上启发式合并 ((DSU) (on) (Tree))算法,如果有不懂的可以提出来。

(quad)本题链接:CF208E Blood Cousins

思路:

(quad)这题的思路几乎和CF570D Tree Requests一样,只需要稍微修改一下,因为是找一个点与多少个点拥有共同的 (K) 级祖先,那么我们就可以先把它的 (K) 级祖先找出来(使用倍增),然后在找这个点有几个 (K) 级后代,或者用深度表示成 (dep_x+k) ,注意这个图不是连通的,每次要清空数组。

[ ext{关于树上启发式合并(Lsu)前置知识} ]

(quad)学这个之前需要对树上操作、 (dfs) 序和轻重链剖分等知识有一定了解,最好已经掌握了树链剖分。

[ ext{算法思想} ]

(quad)树上启发式合并 ((DSU) (on) (Tree)),是一个在 (O(nlogn)) 时间内解决许多树上问题的有力算法,对于某些树上离线问题可以速度大于等于大部分算法且更易于理解和实现。

(quad)先想一下暴力算法,对于每一次询问都遍历整棵子树,然后统计答案,最后再清空cnt数组,最坏情况是时间复杂度为 (O(n^2)) ,对于 (10^5) 的数据肯定是过不去的。

(quad)现在考虑优化算法,暴力算法跑得慢的原因就是多次遍历,多次清空数组,一个显然的优化就是将询问同一个子树的询问放在一起处理,但这样还是没有处理到关键,最坏情况时间复杂度还是 (O(n^2)) ,考虑到询问 (x) 节点时, (x) 的子树对答案有贡献,所以可以不用清空数组,先统计 (x) 的子树中的答案,再统计 (x) 的答案,这样就需要提前处理好 (dfs) 序。

(quad)然后我们可以考虑一个优化,遍历到最后一个子树时是不用清空的,因为它不会产生对其他节点影响了,根据贪心的思想我们当然要把节点数最多的子树(即重儿子形成的子树)放在最后,之后我们就有了一个看似比较快的算法,先遍历所有的轻儿子节点形成的子树,统计答案但是不保留数据,然后遍历重儿子,统计答案并且保留数据,最后再遍历轻儿子以及父节点,合并重儿子统计过的答案。

(quad)其实树上启发式合并的基本思路就是这样,可以看一下代码理解。

il int check(int x)//统计答案
{
  int num=0,ret=0;
  for(re i=1;i<=n;i++)
    {
      if(cnt[i]==num){ret+=i;}
      else if(cnt[i]>num){num=cnt[i],ret=i;}
    }
  return ret;
}
il void add(int x){cnt[col[x]]++;}//单点增加
il void del(int x){cnt[col[x]]--;}//单点减少
il void raise(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}//增加x子树的贡献
il void clear(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}//清空x子树的贡献
il void dfs1(int x,int fa)
{
  dep[x]=dep[fa]+1;father[x]=fa;//处理深度,父亲
  seg[x]=++seg[0];rev[seg[x]]=x;size[x]=1;//子树大小,dfs序
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==fa)continue;dfs1(y,x);
      size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;//重儿子
    }
}
il void dfs2(int x,int flag)//flag表示是否为重儿子,1表示重儿子,0表示轻儿子
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x]||y==father[x])continue;
      dfs2(y,0);//先遍历轻儿子
    }
  if(son[x])dfs2(son[x],1);//再遍历重儿子
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x]||y==father[x])continue;
      raise(y);//更新轻儿子的贡献
    }add(x);//加上x结点本身的贡献
  ans[x]=check(x);//更新答案
  if(!flag)clear(x);//如果是轻儿子,就清空
}

(quad)上面的只是模板的代码,此题的完整代码在下面。(附带注释)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
il void print(int x)
{
  if(x<0)putchar('-'),x=-x;
  if(x/10)print(x/10);
  putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N],go[N],head[N],tot,father[N][20],ans[N],dep[N],son[N],seg[N],rev[N],size[N],cnt[N];
struct node{int k,id;};
vector<node>q[N];
il int Max(int x,int y){return x>y?x:y;}//求较大值
il void Add(int x,int y)//链式前向新
{next[++tot]=head[x];head[x]=tot;go[tot]=y;}
il void add(int x){cnt[dep[x]]++;}//单点增加
il void raise(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}//算上x子树的贡献
il void del(int x){cnt[dep[x]]=0;}//单点减少
il void clear(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}//清空x子树
il void dfs1(int x,int fa)
{
  dep[x]=dep[fa]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
  for(re i=1;i<=18;i++)father[x][i]=father[father[x][i-1]][i-1];//倍增
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      dfs1(y,x);size[x]+=size[y];
      if(size[y]>size[son[x]])son[x]=y;
    }
}
il void dfs2(int x,int flag)
{
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {
      if(y==son[x])continue;
      dfs2(y,0);//先遍历轻儿子
    }
  if(son[x])dfs2(son[x],1);//再遍历重儿子
  for(re i=head[x],y;i,y=go[i];i=next[i])
    {if(y==son[x])continue;raise(y);}//更新轻儿子的贡献
  add(x);//加上x结点本身的贡献
  for(re i=0;i<q[x].size();i++)
    ans[q[x][i].id]=cnt[dep[x]+q[x][i].k];//更新答案
  if(!flag)clear(x);//如果是轻儿子,就清空
}
il int find_father(int x,int y)//找x的第y级祖先
{
  for(re i=18;i>=0;i--){if(y>=(1<<i))y-=(1<<i),x=father[x][i];}
  return x;
}
signed main()
{
  n=read();
  for(re i=1,x;i<=n;i++)x=read(),father[i][0]=x,Add(x,i);
  for(re i=1;i<=n;i++)if(father[i][0]==0)dfs1(i,0);//预处理,倍增数组、dfs序等树上信息
  m=read();
  for(re i=1,x,y,z;i<=m;i++){x=read(),y=read(),z=find_father(x,y);if(z)q[z].push_back((node){y,i});}
  for(re i=1;i<=n;i++)if(father[i][0]==0)dfs2(i,0);//找每棵树的根节点,0表示轻儿子,这样不用手动清空数组
  for(re i=1;i<=m;i++)print(Max(ans[i]-1,0)),putchar(' ');//注意输出要减一,要去除询问节点
  return 0;
}
原文地址:https://www.cnblogs.com/FarkasW/p/14027420.html