一、题意

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

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

二、思路

这题我做了很多遍,换了很多种思路,也看了网上其他人很多题解,仍然没看懂。后来,看了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. APUE学习笔记——10信号——信号接口函数 signal 和 sigaction

    signal函数     signal函数是早起Unix系统的信号接口,早期系统中提供不可靠的信号机制.在后来的分支中,部分系统使用原来的不可靠机制定义signal函数,如 Solaris 10 .而 ...

  2. New Concept English three (23)

    31w 45 People become quite illogical when they try to decide what can be eaten and what cannot be ea ...

  3. git checkout 报错 refname 'origin/branch-name' is ambiguous

    When this happened, it created the file .git/refs/heads/origin/branch-name. So, I just deleted the f ...

  4. cmakelist

    cmake 添加头文件目录,链接动态.静态库 罗列一下cmake常用的命令. CMake支持大写.小写.混合大小写的命令. 1. 添加头文件目录INCLUDE_DIRECTORIES 语法: incl ...

  5. iOS数组的去重,判空,删除元素,删除重复元素 model排序 等

    一: 去重 有时需要将NSArray中去除重复的元素,而存在NSArray中的元素不一定都是NSString类型.今天想了想,加上朋友的帮助,想到两种解决办法,先分述如下. 1.利用NSDiction ...

  6. Git详解之十 Git常用命令

    下面是我整理的常用 Git 命令清单.几个专用名词的译名如下. Workspace:工作区 Index / Stage:暂存区 Repository:仓库区(或本地仓库) Remote:远程仓库 一. ...

  7. js学习笔记知识点

    AJAX用法安全限制JSONPCORS面向对象编程创建对象构造函数原型继承class继承 AJAX 用法 AJAX不是JavaScript的规范,它只是一个哥们“发明”的缩写:Asynchronous ...

  8. TortoiseGit使用入门

    TortoiseGit使用入门 本地使用Git 首先要确定TortoiseGit已找到msysgit,如果先安装msysgit 再装TortoiseGit, 一般TortoiseGit 就会自动的识别 ...

  9. Horizon的本地化

    1. 准备工作 apt-get install gettext horizon源码下载路径 /workspace/horizon/   2. 生成mo文件 django-admin.py makeme ...

  10. HDU4612Warm up 边双连通 Tarjan缩点

    N planets are connected by M bidirectional channels that allow instant transportation. It's always p ...