题解luogu5666 树的重心

Posted teos

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了题解luogu5666 树的重心相关的知识,希望对你有一定的参考价值。

CSP-S2 2019 D2T3

考场上扔了T2来打这题的部分分,然后没看到数据范围是等号,不知道怎么判完全二叉树然后40分滚粗……

----

思路分析

很容易想到$O(n^2)$每次暴力找重心,这个暴力可以用各种神仙方法优化。

通过分析35分的特殊构造分,可以有一个想法,既然特殊构造可以有结论,那么是否也可以有一些结论来解决或者优化整个问题的解法。实际上,通过分析样例可以得到一些性质,这些性质可能有利于问题的求解:

1. 一棵树如果有两个重心,这两个重心一定是相邻的
2. 一棵树的重心一定在根节点所在的重链上
3. 一棵树的重心一定是以该树根节点重儿子为根的子树的重心的祖先

这些性质可以节省下很多不必要的时间。根据性质1,我们可以先找到深度较大的重心,然后对于已求出的重心再判断其父亲是否也是重心(因此接下来说的重心都是深度较大的重心);根据性质2,我们在找重心的时候可以只往根节点所在的重链上找;根据性质3,我们可以从下往上找重心,不用每次重新找。

还有一些性质,接下来对于每种情况具体分析。

删掉一条边后一棵树会变成两部分,设该边的两个端点为$x,y$,其中深度较大的一点为$y$,则这两部分分别为以$y$为根的子树和整棵树减掉以$y$为根的子树。我们先分析以$y$为根的子树。

以$y$为根的子树

我们可以根据性质3预处理出以所有节点为根的子树的重心,只要不断向上走就可以,时间复杂度$O(n)$。

整棵树减掉以$y$为根的子树

根据性质2,这棵树的重心一定在根节点所在的重链上。试想,只要删掉的以$y$为根的子树大小一样,位置实际上是对重心没有影响的,唯一有可能影响的情况就是删掉的这个子树在以根节点重儿子为根的子树中。我们将这些情况再具体分析。

$y$不在以根节点重儿子为根的子树中

这是最平常的情况。既然只有删掉子树的大小对重心有影响,我们可以预处理出删去所有大小的子树之后的重心,然后直接询问。这个预处理只需要从根节点所在的重链从下到上走一遍就可以,时间复杂度$O(n)$。

$y$在以根节点重儿子为根的子树中

试想,如果删掉以$y$为根的子树后,这个重儿子仍然是重儿子,那么重心还是会在原来这条重链上,而重心显然只可能往重链上远离$y$的一端移动,而如果根节点就是重心,删掉子树之后根节点仍然会是重心。因此,最开始我们可以让整棵树的重心作为根节点,处理这种情况就变得很方便。

如果删掉子树后这个重儿子不再是重儿子了怎么办?显然,现在这个重儿子只会是原来的次重儿子。因此,我们可以预处理出次重儿子所在的重链上删去所有大小的子树之后的重心,然后按照$y$不在以根节点重儿子为根的子树中的情况处理。

所有预处理都是$O(n)$,每次询问$O(1)$,总的复杂度$O(n)$。

下面给出具体实现。(由于本人太菜,实现过程可能常数比较大,请勿模仿

具体实现

1.找重心

dfs一遍随便找到一个重心即可。把整棵树的重心作为之后的根节点。

void findroot(int x,int f)
{
    siz[x]=1;
    for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
        if(y!=f)
        {
            findroot(y,x);
            siz[x]+=siz[y];
            if(siz[y]>siz[son[x]])
                son[x]=y;
        }
    if(siz[son[x]]*2<=n && (n-siz[x])*2<=n)
        root=x;
}

findroot(1,0);

2.预处理节点信息

预处理每个节点的子树大小、重儿子、深度、父亲以及属于根节点哪个儿子的子树,同时找到根节点的次重儿子。dfs一遍即可。

void pre(int x,int f)
{
    siz[x]=1,d[x]=d[f]+1,fa[x]=f;//分别表示子树大小、深度以及父亲
    if(f==root)
        ffa[x]=x;
    else
        ffa[x]=ffa[f];//属于根节点的哪个子树
    for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
        if(y!=f)
        {
            pre(y,x);
            siz[x]+=siz[y];
            if(siz[y]>siz[son[x]])
            {
                if(x==root)
                    son2=son[x];
                son[x]=y;//找重儿子
            }
            else
                if(x==root && siz[y]>siz[son2])
                    son2=y;//找根节点的次重儿子
        }
}

memset(son,0,sizeof(son));
pre(root,0);

3.预处理答案

分别预处理出根节点重儿子所在的重链的答案、根节点次重儿子所在的重链的答案以及以每个节点为根的子树的重心。每种情况从下到上走一遍即可。

第一种情况,对于每个删掉子树大小$y$,应该在根节点重儿子所在的重链上找到深度最大的$x$满足$2siz_xgeq n-y$。第二种情况同理。

第三种情况,对于每个子树的根节点$y$,应该其子树内深度最大的$x$满足$siz[x]geq siz[y]$。

void get1(int x,int f)
{
    if(son[x])
        get1(son[x],x);//有重儿子走重儿子
    while(n-2*siz[x]<=nowans && nowans)
    {
        ans1[nowans]=x;
        nowans--;
    }
}//根节点重儿子所在的重链的答案

void get2(int x,int f)
{
    if(x==root)
        nowans=n,get2(son2,x);//走根节点次重儿子
    else
        if(son[x])
            get2(son[x],x);//其余节点有重儿子走重儿子
    while(n-2*siz[x]<=nowans && nowans)
    {
        ans2[nowans]=x;
        nowans--;
    }
}//根节点次重儿子所在的重链的答案


void get3(int r)
{
    if(son[r])
        get3(son[r]);//先走重儿子
    for(int i=head[r],y=ver[i];i;i=Next[i],y=ver[i])
        if(y!=son[r] && d[y]>d[r])
            get3(y);
    int now=son[r]?ans3[son[r]]:r;//从重儿子的重心往上找
    while(siz[now]*2<siz[r])
        now=fa[now];
    ans3[r]=now;
}//以r为根的子树的重心

get1(root,0),get2(root,0),get3(root);

4.枚举删边求答案

实际上枚举删掉子树也是可以的,这里还是用删边来写。

按照前面说到的情况,对于每种情况,先找到一个重心,然后再判断其父亲是否也是重心。判断的时候只要按照重心的定义来判断即可,注意有些特殊的节点要进行特判。

for(int i=1;i<=tot;i+=2)
{
    int x=ver[i],y=ver[ano];
    if(d[x]>d[y])
        swap(x,y);//令y为深度较大的节点
    int h1=ans3[y];
    ans+=h1;//h1就是以y为根的子树的重心
    if(d[fa[h1]]>=d[y] && check1(fa[h1],y))
        ans+=fa[h1];//判断h1的父亲是否也是以y为根的子树的重心
    if(ffa[y]==son[root])//y在以根节点重儿子为根的子树中
        if(siz[son[root]]-siz[y]>=siz[son2])
            ans+=root;//不影响重链,重心为根节点
        else
        {
            ans+=ans2[siz[y]];
            if(check2(fa[ans2[siz[y]]],y))
                ans+=fa[ans2[siz[y]]];
        }//影响重链
    else//y不在以根节点重儿子为根的子树中
    {
        ans+=ans1[siz[y]];
        if(check3(fa[ans1[siz[y]]],y))
            ans+=fa[ans1[siz[y]]];
    }
}

(感觉好多地方可以整合在一起写……凑合着看吧qwq)

下面给出完整代码:

//40分暴力
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define ll long long
#define ano ((i-1)^1)+1
using namespace std;
const int N=3e5+100;
int T,n,cnt,tot;
ll ans;
int head[N],ver[2*N],Next[2*N];
int maxson[N],siz[N],d[N],nowh[N],nowsiz[N];
bool sp[N],v[N];
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
    ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
void dfs(int x,int root)
{
    if(x!=root)
        nowsiz[x]=1;
    v[x]=1;
    for(int i=head[x];i;i=Next[i])
        if(!sp[ver[i]] && !v[ver[i]])
        {
            int y=ver[i];
            dfs(y,root);
            maxson[x]=max(maxson[x],nowsiz[y]);
            if(x!=root)
                nowsiz[x]+=nowsiz[y];
        }
    if(maxson[x]*2<=nowsiz[root] && (nowsiz[root]-nowsiz[x])*2<=nowsiz[root])
        nowh[++cnt]=x;
}//找重心
void pre(int x,int f,int root)
{
    siz[x]=1,d[x]=d[f]+1;
    for(int i=head[x];i;i=Next[i])
        if(!d[ver[i]] && ver[i])
        {
            int y=ver[i];
            if(x==root)
                ffa[y]=y;
            else
                ffa[y]=ffa[f];
            pre(y,x,root);
            siz[x]+=siz[y];
        }
}//预处理子树大小和深度
void clearly()
{
    memset(maxson,0,sizeof(maxson));
    memset(nowsiz,0,sizeof(nowsiz));
    memset(v,0,sizeof(v));
    cnt=0;
}
void solve()
{
    pre(1,0,1);
    for(int i=1;i<=tot;i+=2)
    {
        clearly();
        int x=ver[i],y=ver[ano];
        sp[x]=sp[y]=1;//dfs时不走x,y
        if(d[x]>d[y])
            swap(x,y);//令y为深度较大的节点
        nowsiz[x]=n-siz[y],nowsiz[y]=siz[y];
        dfs(x,x),dfs(y,y);
        sp[x]=sp[y]=0;//还原
        for(int j=1;j<=cnt;j++)
            ans+=nowh[j];
    }
}
void clear()
{
    memset(head,0,sizeof(head));
    memset(Next,0,sizeof(Next));
    memset(siz,0,sizeof(siz));
    memset(d,0,sizeof(d));
    tot=ans=0;
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        clear();
        scanf("%d",&n);
        for(int i=1,x,y;i<n;i++)
        {
            scanf("%d%d",&x,&y);
            add(x,y);
        }
        solve();
        printf("%lld
",ans);
    }
    return 0;
}
//100分
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
#define ano ((i-1)^1)+1
using namespace std;
const int N=3e5+100;
int T,n,tot,root,son2,nowans;
ll ans;
int head[N],ver[2*N],Next[2*N];
int siz[N],son[N],ans1[N],ans2[N],ans3[N],ffa[N],d[N],od[N],fa[N];
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
    ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
void findroot(int x,int f)
{
    siz[x]=1;
    for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
        if(y!=f)
        {
            findroot(y,x);
            siz[x]+=siz[y];
            if(siz[y]>siz[son[x]])
                son[x]=y;
        }
    if(siz[son[x]]*2<=n && (n-siz[x])*2<=n)
        root=x;
}//找重心
void pre(int x,int f)
{
    siz[x]=1,d[x]=d[f]+1,fa[x]=f;
    if(f==root)
        ffa[x]=x;
    else
        ffa[x]=ffa[f];
    for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
        if(y!=f)
        {
            pre(y,x);
            siz[x]+=siz[y];
            if(siz[y]>siz[son[x]])
            {
                if(x==root)
                    son2=son[x];
                son[x]=y;
            }
            else
                if(x==root && siz[y]>siz[son2])
                    son2=y;
        }
}//预处理节点信息
void get1(int x,int f)
{
    if(son[x])
        get1(son[x],x);
    while(n-2*siz[x]<=nowans && nowans)
    {
        ans1[nowans]=x;
        nowans--;
    }
}
void get2(int x,int f)
{
    if(x==root)
        nowans=n,get2(son2,x);
    else
        if(son[x])
            get2(son[x],x);
    while(n-2*siz[x]<=nowans && nowans)
    {
        ans2[nowans]=x;
        nowans--;
    }
}
void get3(int r)
{
    if(son[r])
        get3(son[r]);
    for(int i=head[r],y=ver[i];i;i=Next[i],y=ver[i])
        if(y!=son[r] && d[y]>d[r])
            get3(y);
    int now=son[r]?ans3[son[r]]:r;
    while(siz[now]*2<siz[r])
        now=fa[now];
    ans3[r]=now;
}//预处理答案
void clear()
{
    memset(head,0,sizeof(head));
    memset(Next,0,sizeof(Next));
    memset(ver,0,sizeof(ver));
    memset(son,0,sizeof(son));
    nowans=n,son2=tot=ans=0;
}
bool check1(int x,int y)
{
    return x && siz[son[x]]*2<=siz[y] && (siz[y]-siz[x])*2<=siz[y];
}
bool check2(int x,int y)
{
    if(x==root)
        return siz[son2]*2<=n-siz[y];
    return x && siz[son[x]]*2<=n-siz[y] && (n-siz[y]-siz[x])*2<=n-siz[y];
}
bool check3(int x,int y)
{
    if(x==root)
        return siz[son[x]]*2<=n-siz[y];
    return x && siz[son[x]]*2<=n-siz[y] && (n-siz[y]-siz[x])*2<=n-siz[y];
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d",&n);
        clear();
        for(int i=1,x,y;i<n;i++)
        {
            scanf("%d%d",&x,&y);
            add(x,y);
        }
        findroot(1,0);
        memset(son,0,sizeof(son));
        pre(root,0),get1(root,0),get2(root,0),get3(root);
        for(int i=1;i<=tot;i+=2)
        {
            int x=ver[i],y=ver[ano];
            if(d[x]>d[y])
                swap(x,y);
            int h1=ans3[y];
            ans+=h1;
            if(d[fa[h1]]>=d[y] && check1(fa[h1],y))
                ans+=fa[h1];
            if(ffa[y]==son[root])
                if(siz[son[root]]-siz[y]>=siz[son2])
                    ans+=root;
                else
                {
                    ans+=ans2[siz[y]];
                    if(check2(fa[ans2[siz[y]]],y))
                        ans+=fa[ans2[siz[y]]];
                }
            else
            {
                ans+=ans1[siz[y]];
                if(check3(fa[ans1[siz[y]]],y))
                    ans+=fa[ans1[siz[y]]];
            }
        }//枚举删边求答案
        printf("%lld
",ans);
    }
    return 0;
}

以上是关于题解luogu5666 树的重心的主要内容,如果未能解决你的问题,请参考以下文章

JZOJ6435luoguP5666CSP-S2019树的重心

luoguP5666 树的重心 树状数组

CSP2019 树的重心 题解

牛客白月赛8题解

[常见做法整合]CSP-S2019 D2T3 树的重心

树的重心