树上启发式合并——学习笔记

学习背景

在学习这块内容前,最好要先了解树的轻重链划分(了解过树剖)

题目特征

树上启发式合并,通常是在题目给定了根节点 r o o t root (通常 r o o t = 1 root=1 ),在离线情况下,解决查询某个子树下符合题目条件的答案,并且子树的信息是没法全部存下来的,如查询 u u 及其子树中不同颜色个数
下面还有很多例题,没明白意思可以先看下都是什么题

树上启发式合并思想

暴力就不用说了,直接搜那个点的子树,单次查询复杂度有 O ( n ) O(n) M M 次查询就到了 O ( N M ) O(NM)
而树上启发式合并能够做到 d f s dfs 一遍整棵树即可得到所有答案(离线的询问),复杂度在 O ( N l o g N ) O(NlogN)

树上启发式合并非常暴力,在一次 d f s dfs 的过程中,统计两遍轻儿子,统计一遍重儿子就能实现
对于父亲节点 u u 来说,他的信息通过合并子节点 v v 的信息来得到,那么这么说我们记录每个点所有的信息不就完了吗?可是这类题目显然是不会给你这个机会的,一般需要记录的信息非常多,每次都记录一下根本实现不了,所以这里我们需要有取舍的保留子节点的信息!,即我们每次保存当前点最后一次搜索的儿子信息,然后再重新搜索其他儿子的信息并且合并上去
对于父亲节点来说,显然最后一次搜索子节点的信息是可以保留的,因为回来我们就要搜父亲的信息了,而这个点的信息不就是父亲的信息吗,所以最后一次搜索子节点非常关键!
而这里我们最后一次搜的子节点就是重儿子,为什么选重儿子呢?因为重儿子是所有子节点里面点的数量最多的,所以包含的信息也最多,我们如果要重新搜索他的话花费的时间要比轻儿子要更多

所以这里的实现过程就是
对于一个点 u u ,先统计完他所有轻儿子的答案,并且在最后回溯到 u u 的时候要删去保存的信息(否则会影响我们搜索重儿子的答案统计)
然后我们再搜重儿子,查询答案并且保存信息(为什么保留信息上面解释了)
最后我们再重新搜一遍轻儿子,保存信息即可(不需要更新答案)

复杂度分析

①一个点的 u p d upd 只会经过轻边
②树剖中轻重链划分的性质——从根结点到任意结点的路所经过的轻重链的个数必定都小于 l o g n logn
换一个角度来想,就是一个点最多有 l o g N logN 个祖先节点会需要他的信息,那么 N N 个点就是 O ( N l o g N ) O(NlogN)

模板

void dfs(int u, int fa) {//轻重链划分
    siz[u] = 1;
    for (auto &v: g[u])
        if (v != fa) {
            dfs(v, u);
            siz[u] += siz[v];
            if (!son[u] || siz[v] > siz[son[u]])
                son[u] = v;
        }
}

void upd(int u, int fa, int k) {//将信息加入/撤回
    //k = 1的时候是加上信息,k = -1的时候是撤回信息
    
    for (auto &v: g[u])
        if (v != fa && !vis[v]) upd(v, u, k);
}

void dsu(int u, int fa, int keep) {//u为当前点, fa为父节点, keep为是否保留信息
    for (auto &v: g[u])//先搜我们的轻儿子,过程中不保留信息
        if (v != fa && v != son[u]) dsu(v, u, 0);
    if (son[u]) dsu(son[u], u, 1), vis[son[u]] = 1;//然后搜我们的重儿子, 过程中保留信息
    upd(u, fa, 1);//然后再查一遍我们的轻儿子,把信息更新上去
    //这里用来统计答案
    
    if (son[u]) vis[son[u]] = 0;
    if (!keep) upd(u, fa, -1);//如果信息不保留,那我们就撤回
}

练习题

1. CF600E - Lomsat gelral

题意

N N 个节点以点 1 1 为根的树,每一个节点有一个颜色 c o l o r i color_i ,对于一个以 u u 为根的子树来说,他的子树中的节点有多种颜色,现在求出出现次数最多的颜色(可能有多个颜色出现次数相同),并将出现次数求和
输出以所有点的答案

分析

以点1为根,我们需要保存的信息为当前点子树中颜色出现次数,并且在统计答案的时候找到颜色最多的
这里我们可以用一个桶 c n t [ i ] cnt[i] 来记录颜色 i i 出现的次数,然后用 m x mx 来记录当前次数最多的颜色, s u m sum 记录次数最多的颜色的次数之和
这样当某种颜色出现次数 c n t > m x cnt>mx 的时候,我们将 m x mx 更新,并且 s u m = c n t sum=cnt (为什么不是加上答案呢,因为我们最大值已经更新了,所以实际上是 s u m = 0 , s u m = s u m + c n t sum=0, sum = sum + cnt )
c n t = m x cnt=mx 的时候, m x mx 不必更新, s u m + = c n t sum+=cnt
c n t < m x cnt<mx 的时候,啥都不用做

代码

部分代码下面, 全部代码这里

int cnt[MAX], vis[MAX], mx;//mx记录当前
ll ans[MAX], sum;
void upd(int u, int fa, int k) {
    cnt[color[u]] += k;
    if (k == 1 && cnt[color[u]] >= mx) {
        if (cnt[color[u]] > mx) mx = cnt[color[u]], sum = 0;
        sum += color[u];
    }
    for (auto &v: g[u])
        if (v != fa && !vis[v]) upd(v, u, k);
}

void dsu(int u, int fa, int keep) {
    for (auto &v: g[u])
        if (v != fa && v != son[u]) dsu(v, u, 0);
    if (son[u]) dsu(son[u], u, 1), vis[son[u]] = 1;
    upd(u, fa, 1);
    ans[u] = sum;
    if (son[u]) vis[son[u]] = 0;
    if (!keep) upd(u, fa, -1), mx = sum = 0;
}

2. CF570D - Tree Requests 题解

3. Sgu507 题解

4. HackerEarth The Grass Type 题解

5. CF246E - Blood Cousins Return 题解

6. CF208E - Blood Cousins 题解

7. IOI2011 Race 题解

8. CF1009F - Dominant Indices 题解

9. CF375D - Tree and Queries 题解

参考blog

[Tutorial] Sack (dsu on tree)
树上启发式合并
dsu on tree学习笔记

猜你喜欢

转载自blog.csdn.net/weixin_44282912/article/details/104506644