CodeForces 708C 题解

题解笔记

CF708C Centroids

时间限制 \(4s\) | 内存限制 \(500MB\)

题目描述:

给定一棵树,你有一次将树改造的机会,改造的意思是删去一条边,再加入一条边,保证改造后还是一棵树。

请问有多少点可以通过改造,成为这棵树的重心?(如果以某个点为根,每个子树的大小都不大于 \(\lfloor \dfrac{n}{2} \rfloor\),则称某个点为重心)

输入格式:

第一行一个正整数 \(n\)\(2 \leq n \leq 400000\)

后面 \(n-1\) 行描述这一棵树的各条边

输出格式:

输出 \(n\) 个数,用空格隔开,第 \(i\) 个数表示第 \(i\) 个点在树经过改造后能否成为重心

输入输出样例:

样例1输入 样例1输出 样例2输入 样例2输出
3
1 2
2 3
1 1 1 5
1 2
1 3
1 4
1 5
1 0 0 0 0

解题思路:

首先分析题目中的“改造”,如何改造?

假设我们想让 \(u\) 成为重心(\(u\) 不是重心)

考虑题目中重心的定义, \(u\) 不是重心肯定是它的某一个子节点的子树超过了\(\lfloor \dfrac{n}{2} \rfloor\)

注意:一定是一棵这样的子树超标,如果两棵子树都超标,那总大小就至少为 \(\lfloor \dfrac{n}{2} \rfloor+1+\lfloor \dfrac{n}{2} \rfloor+1+1\) ,显然大于了 \(n\)

所以要想让 \(u\) 成为重心,就需要从超标子树中抽取一棵符合要求的子树,将它连到 \(u\)

这时如果超标子树依然超标,那就恭喜你,这个点改造后也成不了重心

否则就可以为重心

接下来考虑如何快速求出某一点的超标子树,

发现题目里还有一个点没有被利用,就是这是一棵无根树,

于是我们想到对于每一个点来说,它可以有多个子节点,却只有一个父节点,所以考虑让每一个点的父节点以上的子树超标

为了下文方便,我们将 \(u\) 的父节点以上的子树称为 \(u\) 的向上子树

例如下图中 \(2\) 的向上子树如下所示:

所有加粗的节点都是 \(2\) 的向上子树里的

而对于什么样的树每个节点的子节点的子树都不会超标

那就是以重心为根的树

所以就先写一个 dfs 用于找根,就是找重心,这里为了好找,先将根设为 \(1\)

(详细代码请见《参考代码》)

下文将重心称为 rt

接下来,开始计算以 rt 为根的树的各个子树的大小,即 sz 数组

这就是第二次 dfs 的第一个目的

接下来开始分析 dp ,受到上文分析的启发,可以想到设 dp[i]\(i\) 的向上子树中最大的不超标的子树

那对于 \(i\) ,判断它是否可以被改造为重心就可以用 n-sz[i]-dp[i]<=n>>1||i==rt 来计算

为了计算 dp 只好再建立一个辅助数组 mxt[i] 表示子树 \(i\) 中最大的不超过 \(\lfloor \dfrac{n}{2} \rfloor\) 的子树

然后在 dpdfs 的过程中,维护 famx 变量,表示祖先中最大子树的大小

所以 dp[u]=famx ,接下来如果向上子树大小不超标,就可以 famx=max(famx,n-sz[u])

这里的 n-sz[u] 就是向上子树的大小

然后遍历子节点,如何用 mxt 更新 famx 呢?

如下所示:

如图所示,我们从 \(2\) 遍历子节点,遍历到了 \(8\) ,毫无疑问,\(8\)的祖先中最大的非超标子树应该是 \(3\) 往下的子树,可以发现,这里的子树不是向上子树,所以在更新 famx 时也需要 mxt ,这里的子树正是 mxt[1]

所以 dpdfs(u,v,max(famx,sz[mxt[u]]))

就完成了吗?

如果觉得完成了就过于相信题解了

显然没有,如果 mxt[u]==v 怎么办呢?

所以需要维护 mxt[u] 的最大值以及次大值,将 mxt 数组扩一维:mxt[i][0/1]

然后再 dpdfs 就行了

mxt 数组再什么时候计算呢?

运行 cntsz 函数时一起计算了,就可以了

参考代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=4e5+7;
int n,dp[MAXN],mxt[MAXN][2],sz[MAXN],w=0x3f3f3f3f,rt;
//dp[i]表示除了子树i外最大的子树大小(由于以rt为根,所以大小不会超过n/2)
//mxt用于求主dp的辅助dp,mxt[i]表示子树i中最大以及次大的不超过n/2的子树

struct Edge
{
    int v,nxt;
} e[MAXN<<1];
int head[MAXN],tot;

void add(int u,int v)
{
    e[++tot].v=v;
    e[tot].nxt=head[u];
    head[u]=tot;
}

int find_rt(int fa,int u)//第一次dfs,寻找重心,假定1为根,返回当前子树的大小
{
    int sum=1,uw=0;
    for(int i=head[u];i;i=e[i].nxt)
    {
        int v=e[i].v;
        if(v!=fa)
        {
            int s=find_rt(u,v);
            if(s==-1) return -1;
            sum+=s;
            uw=max(uw,s);
        }
    }
    uw=max(uw,n-sum);
    if(uw<=n>>1) return rt=u,-1;
    return sum;
}

void cntsz(int fa,int u)//计算size,顺便维护mxt数组
{
    sz[u]=1;//将u节点自己包含进去
    for(int i=head[u];i;i=e[i].nxt)
    {
        int v=e[i].v;
        if(v==fa) continue;
        cntsz(u,v);
        sz[u]+=sz[v];
        if(sz[v]<=n>>1)
        {
            if(sz[v]>sz[mxt[u][0]]) mxt[u][1]=mxt[u][0],mxt[u][0]=v;
            else if(sz[v]>sz[mxt[u][1]]) mxt[u][1]=v;
        }
    }
}

void dpdfs(int fa,int u,int famx)//通过dfs进行dp;famx表示祖先中最大子树的大小
{
    dp[u]=famx;
    if(n-sz[u]<=n>>1) famx=max(famx,n-sz[u]);
    for(int i=head[u];i;i=e[i].nxt)
    {
        int v=e[i].v;
        if(v==fa) continue;
        if(v==mxt[u][0]) dpdfs(u,v,max(famx,sz[mxt[u][1]]));
        else dpdfs(u,v,max(famx,sz[mxt[u][0]]));
    }
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<n;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        add(u,v);
        add(v,u);
    }
    find_rt(0,1);
    //printf("rt:%d\n",rt);
    cntsz(0,rt);
    dpdfs(0,rt,0);
    //for(int i=1;i<=n;i++) printf("%d:sz:%d,dp:%d\n",i,sz[i],dp[i]);
    for(int i=1;i<=n;i++) printf("%d ",n-sz[i]-dp[i]<=n>>1||i==rt);
    return 0;
}

posted @ 2022-01-27 11:37  yhang323  阅读(106)  评论(0)    收藏  举报