[SMOJ2104]树

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;
}


阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013686535/article/details/77219005
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭