[SMOJ2104]树

来源:互联网 发布:网络分析仪器 编辑:程序博客网 时间:2024/06/06 00:38

60%:
做法一:直接考虑从每个结点出发,向下 dfs 考察统计。
时间复杂度(上限):O(n2)
做法二:树形 DP。记 f[i][j] 为以 i 为根的子树中种类 j 的结点个数,则跑一遍 dfs 即可,但转移时要把子树每一种类的值都合并到根的值。
时间复杂度:O(nk),其中 k 为种类个数。

100%:
稍微结合一下做法一和做法二就可以了。
也就是说,在适当的时候用暴力,但也要能够可以把子结点的结果合并给父亲。这就要用到“树链”——启发式合并。
以样例为例,进行说明。先预处理一下,对树上的边进行轻重剖分:

这一步,跑一遍 O(n) 的 dfs 即可完成。同时还要记录一下各结点的父亲(方便后面的处理),同时记下所有叶子结点:2、4 和 5。
接下来,我们抛弃做法二里面用二维数组标记的繁琐方法,现在我们只需要两个数组。用 fi 表示当前统计出的种类 i 的结点个数,ti 表示时间戳。

首先,从各叶子结点开始,不断向父亲 dfs,直到遇到轻边。一开始应该从 2 出发,时间戳为 t1=1(表示 fb2 中的值是从第一个叶子向上得到的)。
结点 2 的类型为 1,点权为 3。则 f1=3。因为是叶子结点,没有儿子,因此 ans2=fb2=3。考虑到 2 到 1 的边为轻边,不再向上,结束第 1 次。
(当前时间戳变为 2)结点 4 的类型为 1,点权为 5。发现 t1=1<2,说明是之前旧的信息,清空 f1,并累加 5 进去。因为是叶子结点,没有儿子,因此 ans4=fb4=5。考虑到 4 到 3 的边为重边,继续向上。
到达结点 3,类型为 2,点权为 4。则 f2=4t2=2。因为是从 4 上来的,已经包含了它的信息,不要重复考察。
而 5 是轻儿子,暴力 dfs 统计,f2+=6。考察完,返回 3 之后,统计得到总答案为 10。考虑到 3 到 1 的边为重边,继续向上。
到达根结点,类型为 1,点权为 2。发现 t1=2,说明当前所统计的类型 1 的信息是最新的,直接累加上去,fb1+=a1=7
而 1 还有一个轻儿子 2,暴力统计一下,f1+=3。得到答案 ans1=10。显然根结点没有父亲了,本次 dfs 结束。
(当前时间戳变为 3)结点 5 的类型为 2,点权为 6。发现 t2=2<3,说明是之前旧的信息,清空 f2,并累加 6 进去。因为是叶子结点,没有儿子,因此 ans5=fb5=6。考虑到 5 到 3 的边为轻边,不再向上。
这样我们就算出了全部的答案 {10, 3, 10, 5, 6}。

而这种方法的时间复杂度也很低,根据比赛总笔记里的分析,每个点被暴力统计最多不会超过 log2n 次,因此总的时间复杂度可以认为是 O(nlog2n) 级别的。
总结一下,这种路径剖分的方法,妙处主要有:

  • 对重边连接的子树信息,可以直接拿来就加,因为它是 size 最大的儿子,暴力肯定不合算。而对于其他相对较轻的儿子,则可以暴力解决。注意,由于结点的信息是依赖于重子结点的,因此计算顺序应该自底向上,这就是为什么要从叶子结点出发。
  • 利用时间戳,可以快速判断某些信息是否仍然适用。如果与当前毫无关联(如上例中的 2 与 4),累加就会导致答案错误,但直接全部统一清零一次又会导致太慢(当然,本题直接 memset 似乎可以卡过),这时用时间戳的判断方法就可以根据需要快速清空特定的值。

本题如果不用路径剖分的方法,还可以利用差分思想进行计算,同样是可以 AC 的。

参考代码:

#include <algorithm>#include <cstdio>#include <cstdlib>#include <cstring>#include <iostream>using namespace std;const int MAXN = 1e5 + 100;struct Edge {    Edge *next;    int dest, typ; //typ = 0 为轻边,1 为重边} edges[MAXN << 1], *current, *first_edge[MAXN], *previous[MAXN];int n, current_time, leaves_cnt;int a[MAXN], b[MAXN], siz[MAXN], leaves[MAXN], num[MAXN], timestamp[MAXN], ans[MAXN];Edge *counterpart(Edge *x) {    return edges + ((x - edges) ^ 1);}void insert(int u, int v) {    current -> next = first_edge[u];    current -> dest = v;    current -> typ = 0; //初始时默认为轻边    first_edge[u] = current ++;}void init_dfs(int root, int pre) { //第一遍预处理的 dfs//  printf("%d %d\n", root, pre);    siz[root] = 1; Edge *heavy_edge = NULL; //指向重儿子 (如果存在) 的边    for (Edge *p = first_edge[root]; p; p = p -> next) {        int v = p -> dest;        if (v != pre) {            init_dfs(v, root);            siz[root] += siz[v]; previous[v] = counterpart(p); //previous 为指向父亲的边            if (!heavy_edge || siz[v] > siz[heavy_edge -> dest]) heavy_edge = p; //取 size 最大的儿子        }    }    if (siz[root] == 1) leaves[leaves_cnt++] = root; //统计叶子结点    else heavy_edge -> typ = counterpart(heavy_edge) -> typ = 1; //标记重边}void brute_force(int root) { //对轻子树的暴力求解    if (timestamp[b[root]] < current_time) { //如果是过时的信息则要清空        num[b[root]] = 0;        timestamp[b[root]] = current_time; //并打上新的时间戳    }    num[b[root]] += a[root];    for (Edge *p = first_edge[root]; p; p = p -> next)        if (p != previous[root]) brute_force(p -> dest);}void tree_train(int root) { //从叶子结点不断向上的过程    if (timestamp[b[root]] < current_time) { //同理        num[b[root]] = 0;        timestamp[b[root]] = current_time;    }    num[b[root]] += a[root];    for (Edge *p = first_edge[root]; p; p = p -> next)        if (p != previous[root] && !p -> typ) brute_force(p -> dest); //对轻子树则暴力求解    //因为是自底向上的,所以重儿子的信息之前已经累加在了 num[] 数组中    ans[root] = num[b[root]];    if (root != 1 && previous[root] -> typ) tree_train(previous[root] -> dest); //注意要判断有父亲才能向上,不然在根结点会爆炸    else return ; //轻边则停止本次}int main(void) {    freopen("2104.in", "r", stdin);    freopen("2104.out", "w", stdout);    scanf("%d", &n); current = edges;    fill(first_edge, first_edge + n + 1, (Edge*)0);    for (int i = 1; i <= n; i++) scanf("%d%d", &a[i], &b[i]);    for (int i = 1; i < n; i++) {        int x, y; scanf("%d%d", &x, &y);        insert(x, y); insert(y, x);    }//  puts("build_tree");    init_dfs(1, 0); //puts("init_dfs");    memset(timestamp, -1, sizeof timestamp);    for (int i = 0; i < leaves_cnt; i++) {//      printf("%d\n", i);        current_time = i; //更新当前时间        tree_train(leaves[i]);    }    for (int i = 1; i <= n; i++) printf("%d ", ans[i]);    return 0;}