一、题意

给定一颗树,对于每一个节点,判断能否在树中删除某一条边,然后在任意两个节点之间加一条边,使这个点成为重心。

注:删除树中某一条边后,标程并不会这么无聊地把这棵树变成两个孤立的连通图,而是再让所有节点组合成一棵树。如果在本来就连通的节点之间再连一条边,形成一个图,那必定会造成所有节点之间不能相互连通,那当前枚举的节点肯定不能成为重心。而如果再形成一个图,当前枚举的节点还有可能成为重心,何乐而不为呢?

二、思路

这题我做了很多遍,换了很多种思路,也看了网上其他人很多题解,仍然没看懂。后来,看了Codeforces上的官方思路以及和一位朋友讨论了一番之后,终于明白这题的解法了。网上有很多方法,但仍然很难看懂,也许是现在阅读代码的能力还不够吧。所以,我在这里尽自己最大的表达能力把自己的思路详细地讲解清楚,并附上代码。代码有些冗长,但是,我自认为,绝对是浅显易懂的。
首先,以1号节点为树根,找出给定树的重心,假设为C;

找到重心C后,把C当作整棵树的树根。

然后,由于在第一步找重心的过程中,以每个节点为根的子树的节点个数已经计算出来了,但是那是在以1为根的情况下计算出来的。而现在真正的树根是重心C,所以,需要以重心C为树根再次计算出每个节点的子树的节点个数。我把他们放在了amts数组里面。含义为:amounts。

由树的重心的定义可知,重心C的所有子树的节点个数都不超过n/2。而重心C的子节点下子、孙、曾孙等节点,以它们为根的子树的节点个数更小于n/2。所以,当枚举每一个非重心节点v,判断它能否通过上述操作使v变成重心时,amts[v]肯定是<=n/2的。也就是说,以v为根的子树的节点个数肯定不超过n/2。所以,我们只需要判断能否在v节点的上面找到一条边,干掉这条边,再从两个断点中找一个点连一条边到节点v上,使得v是重心。特别要注意这里:为什么时是连接到v上,而不是连接到v上面的其他节点,或者v的子或孙或曾孙等节点呢?关于这个问题,我也一直被困扰着。现在终于比较明白了。要注意,我们是枚举,每一节点都有机会被轮到,如果从断点中连一条边到别的地方,可能接下来某一次又有可能从断点处连一条边到这个节点v,这叫做打乱仗,彻底乱套了。这做题压根没法做下去了。再说一遍,我们是枚举,对于当前枚举的节点v,我们就只考虑和当前节点v有关系的操作,而不是枚举我的时候连你,枚举你的时候连他。

然后接下来就是删边的问题了。删哪条边呢?首先,剔除节点v以下的部分,因为它们加起来的数量也不超过n/2,删除一条边,再连一条到v,v子树以外的树根本没动。然后把整棵树画标准,重心在最顶上,叶子节点在最下面。从最下层的边开始找,假设最下层边的层号为f。删除f层的一条边,还不如删除f-1层的一条边,因为删除f-1层的一条边可以把出v子树以外的可能超过n/2个节点平分地更加均匀。如下图所示。

当前枚举的节点是6时,我与其删掉最下层的e(4, 8)或e(5,11)等,还如果删掉e(2,4)或e(1,2),因为这样可以使除节点6、12和13外的所有节点平分地更加均匀,所以,很显然,删掉重心C和它的子节点之间的边是最合适的。因为被独立出来的这一个联通块所包含的节点树最多,更接近于n/2。

好,现在问题又来了。

1、当我枚举每一个节点的时候,都需要去查找重心C的子节点中节点数最多的那棵树,假设为T,这样时间复杂度是O(n^2),显然超时。但是,这很容易优化,因为T是不变的,只要找一次就行了。所以,只要记录好这棵树的节点编号就行。

2、如果当前枚举的节点在T中,这又怎么搞呢?如果这样,那就还需要再记录重心C的子节点中节点数第二多的一棵树的根节点编号,假设为T2。如果当前枚举的节点在子树T中,那么,就删掉T2和重心C的连线,然后从子树T2连一条边到节点v来,即让子树T2成为v的孩子。否则,就删掉T和重心C的连线,然后从子树T连一条边到节点v来,即让子树T成为v的孩子。然后再判断n
- 以节点v为根的子树的节点数 - T子树或T2子树的节点数
是否小于等于n/2。如果是,说明节点v可以通过上述操作成为重心,否则,不行。

3、如果有多个重心怎么办,选哪一个?这很好办,任意选取一个即可。而选择哪一个不用我们来做抉择。

当然,如果只这样,很快就会被样例卡掉。比如这么一棵树。

显然,重心是2或3。假设程序帮我们选取的是3。3的最大子树的编号是2,第二大的是4(4和7是一样的)。如果使用上面的算法,当枚举到6的时候,由于6在最大子树中,所以,就从第二大的子树4中连一条边过来,删除3和4之间的边。那么,n
- 以节点v为根的子树的节点数 - T子树或T2子树的节点数
= 10 - 2 -  2 = 6 > 10 / 2 = 5,所以,判定6号节点不能通过以上操作成为重心。而实际上,应该把删除3和2之间的连线,然后把3连到6上来,然后6可以成为重心。

所以,上述算法还有不够完善的地方:如果当前枚举的节点v在最大子树T中,应该尝试两种删边的方法。①:删除重心和第二大的子树T2之间的连线,让T2成为节点v的孩子;②:删除重心和最大的子树T之间的连线,此时,判断v是否可以成为重心的算式变成:n
- T子树的节点数是否小于等于n/2。也就是说,我们需要尝试让重心成为节点v的孩子,看看是否能让v成为重心。

思路大致如此,如果您不明白或者有疑问,可以联系我的QQ邮箱:565261641@qq.com。我们可以一起讨论。

三、代码

#include<bits/stdc++.h>
using namespace std;
;
typedef struct {
    int to, next;
} Edge;
Edge tree[MAXN * ];
int head[MAXN], cnt;
int n;
bool ans[MAXN];
/**重心节点编号*/
int centroid;
/**
    amts[i]:以i为根节点的子树的节点个数。
    maxSubamt[i]:以i的子节点为根节点的子树的最大节点个数。
*/
int amts[MAXN], maxSubamt[MAXN];
/**
    重心的两个子节点,这两个子节点的amt是最大和次大的。
*/
struct Son {
    set<int> subset;
    int id;
    int amt;
} maxSon[];

void add(int from, int to) {
    tree[cnt].to = to;
    tree[cnt].next = head[from];
    head[from] = cnt++;
}

void init() {
    memset(head, -, sizeof(head));
    memset(ans, , sizeof(ans));
    cnt = ;
    centroid = -;
    maxSon[].amt = maxSon[].amt = ;
    maxSon[].subset.clear();
    maxSon[].subset.clear();
}

/**找出重心*/
void find_centroid(int root, int par) {
    amts[root] = , maxSubamt[root] = ;
    ; i != -; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            find_centroid(to, root);
            amts[root] += amts[to];
            maxSubamt[root] = max(maxSubamt[root], amts[to]);
        }
    }
    )centroid = root;
}

/**重新计算对于每个节点以它为根节点的子树的节点个数。因为树根不再是1,而是重心centroid。*/
void recalculate_amts(int root, int par){
    amts[root] = ;
    ; i != -; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            recalculate_amts(to, root);
            amts[root] += amts[to];
        }
    }
}

/**找出重心的子节点中,节点数最多和次多的两个子节点。*/
void find_max2sons(int root, int par) {
    ; i != -; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            ].amt < amts[to]) {
                maxSon[] = maxSon[];
                maxSon[].amt = amts[to];
                maxSon[].id = to;
            } ].amt < amts[to]) {
                maxSon[].amt = amts[to];
                maxSon[].id = to;
            }
        }
    }
}

/**找到son[0]的所有子节点,包括它自己 。*/
void find_subset(int root, int par) {
    maxSon[].subset.insert(root);
    ; i != -; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            find_subset(to, root);
        }
    }
}

/**
    对于非重心的每个节点,都尝试三种可能的情况。
*/
void dfs_ans(int root, int par) {
    if(root == centroid)ans[root] = true;
    else {
        ].subset.count(root)) {
            ].amt <= n / )ans[root] = true;
            ].amt <= n / )ans[root] = true;
        } ].amt <= n / )ans[root] = true;
    }
    ; i != -; i = tree[i].next) {
        to = tree[i].to;
        if(to != par) {
            dfs_ans(to, root);
        }
    }
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("Dinput.txt", "r", stdin);
    //freopen("Doutput2.txt", "w", stdout);
#endif // ONLINE_JUDGE
    int a, b;
    while(~scanf("%d", &n)) {
        init();
        ; i < n; ++i) {
            scanf("%d%d", &a, &b);
            add(a, b);
            add(b, a);
        }
        find_centroid(, -);
        recalculate_amts(centroid, -);
        find_max2sons(centroid, -);
        ) {
            find_subset(maxSon[].id, centroid);
            dfs_ans(centroid, -);
        }
        ; i <= n; ++i)printf( : , i < n ? ' ' : '\n');
    }
    ;
}

四、总结

1、在树形DP中,思路不能混乱,一定搞清楚层次关系。对于某个操作,如果它需要对每个节点都操作一次,那么,在编写代码的时候,就无需考虑该操作和其他节点的关系。否则,逻辑关系、思路将会变得一团糟。以至于让一个简单的程序变得非常复杂。

2、由于树本身的特点,当我们需要选取某个节点v的两个子节点时(比如选两个节点数最多的子节点),压根不用考虑是否会重复的问题。因为程序在一次搜索中不会遍历一个节点两次,所以,不会出现同一个节点被统计两次的情况。所以,如果是选两个节点数最多的子节点,最大的那个不断更新,发现比已记录的最大的值更大的值时,把原来记录的给第二大的值。否则,通过max更新第二大的值。

附:树的测试数据生成器代码

#include<bits/stdc++.h>
using namespace std;

int t;
int n, m;
int a, b, c, d;

int random1(int mod) {
    );
    ) % mod + ;
}

int main() {
    freopen("input.txt", "w", stdout);
    srand(time(NULL));
    t = ;
    printf("%d\n", t);
    ; i < t; ++i) {
        n = random1();
        printf("%d\n", n);
        ; k <= n; ++k)printf(), k);
        printf("\n");
    }
}

Codeforces-708C(树形DP)的更多相关文章

  1. Codeforces 1153D 树形DP

    题意:有一个游戏,规则如下:每个点有一个标号,为max或min, max是指这个点的值是所有子节点中值最大的那一个,min同理.问如何给这颗树的叶子节点赋值,可以让这棵树的根节点值最大. 思路:很明显 ...

  2. Codeforces 1088E 树形dp+思维

    比赛的时候看到题意没多想就放弃了.结果最后D也没做出来,还掉分了,所以还是题目做的太少,人太菜. 回到正题: 题意:一棵树,点带权值,然后求k个子连通块,使得k个连通块内所有的点权值相加作为分子除以k ...

  3. Codeforces 1179D 树形DP 斜率优化

    题意:给你一颗树,你可以在树上添加一条边,问添加一条边之后的简单路径最多有多少条?简单路径是指路径中的点只没有重复. 思路:添加一条边之后,树变成了基环树.容易发现,以基环上的点为根的子树的点中的简单 ...

  4. CodeForces - 337D 树形dp

    题意:一颗树上有且仅有一只恶魔,恶魔会污染距离它小于等于d的点,现在已经知道被污染的m个点,问恶魔在的可能结点的数量. 容易想到,要是一个点到(距离最远的两个点)的距离都小于等于d,那么这个点就有可能 ...

  5. CodeForces 219D 树形DP

    D. Choosing Capital for Treeland time limit per test 3 seconds memory limit per test 256 megabytes i ...

  6. codeforces 337D 树形DP Book of Evil

    原题直通车:codeforces 337D Book of Evil 题意:一棵n个结点的树上可能存在一个Evil,Evil危险范围为d,即当某个点与它的距离x<=d时,那么x是危险的. 现已知 ...

  7. Up and Down the Tree CodeForces - 1065F (树形dp)

    链接 题目大意:给定$n$结点树, 假设当前在结点$v$, 有两种操作 $(1)$移动到$v$的子树内任意一个叶子上 $(2)$若$v$为叶子, 可以移动到距离$v$不超过$k$的祖先上 初始在结点$ ...

  8. codeforces 1053D 树形DP

    题意:给一颗树,1为根节点,有两种节点,min或者max,min节点的值是它的子节点的值中最小的,max节点的值是它的子节点的值中最大的,若共有k个叶子,叶子的值依次为1~k. 问给每个叶子的值赋为几 ...

  9. Codeforces 1120D (树形DP 或 最小生成树)

    题意看这篇博客:https://blog.csdn.net/dreaming__ldx/article/details/88418543 思路看这篇:https://blog.csdn.net/cor ...

  10. Codeforces 735E 树形DP

    题意:给你一棵树,你需要在这棵树上选择一些点染成黑色,要求染色之后树中任意节点到离它最近的黑色节点的距离不超过m,问满足这种条件的染色方案有多少种? 思路:设dp[x][i]为以x为根的子树中,离x点 ...

随机推荐

  1. WEB-INF目录下的文件访问权限

    对于Tomcat服务器而言,WEB-INF是个特殊的目录.这个目录并不属于Web应用程序可以访问的上下文路径的一部分, 对于客户端来说,这个目录是不可见的,不能通过在浏览器中直接输入地址的方式来访问. ...

  2. Leetcode 894. All Possible Full Binary Trees

    递归 # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val = x # ...

  3. 关于HslCommunication组件的双模式客户端的说明,此说明适用于所有的派生类客户端,包括三菱,西门子,欧姆龙,modbustcp,机器人,simplifyNet客户端等等

    前言 本文主要是答疑文章,针对广大网友非常频繁的提问而总结的问题 nuget地址:https://www.nuget.org/packages/HslCommunication/            ...

  4. 用vue实现百度搜索功能

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. 高并发异步uwsgi+web.py+gevent

    为什么用web.py? python的web框架有很多,比如webpy.flask.bottle等,但是为什么我们选了webpy呢?想了好久,未果,硬要给解释,我想可能原因有两个:第一个是兄弟项目组用 ...

  6. 【剑指offer】数组中的逆序对。C++实现

    原创文章,转载请注明出处! 博客文章索引地址 博客文章中代码的github地址 # 题目 # 思路 基于归并排序的思想统计逆序对:先把数组分割成子数组,再子数组合并的过程中统计逆序对的数目.统计逆序对 ...

  7. SaltStack简明教程

    第1章 SaltStack简明教程 1.1 SaltStack简介 SaltStack是基于Python开发的一套C/S架构配置管理工具(功能不仅仅是配置管理,如使用salt-cloud配置AWS E ...

  8. 重温CLR(十四) 可空类型

    我们知道,一个值类型的变量永远不可能为null.它总是包含值类型本身.遗憾的是,这在某些情况下会成为问题.例如,设计一个数据库时,可将一个列定义成为一个32位的整数,并映射到FCL的Int32数据类型 ...

  9. 船长带你看书——《selenium2 python 自动化测试实战》(2)浏览器操作

    浏览器操作 # coding: utf-8 from selenium import webdriver from time import sleep driver = webdriver.Firef ...

  10. pymongo和mongodbengine之间的区别

    pymongo是一个mongo driver,可以用来连接数据库以及对数据库进行操作,但是是用mongo自己的用来操作数据库的语句进行操作数据库,而mongodbengine就像是sqlalchemy ...