树上启发式合并总结

前言

某一天发现一道树上启发式合并裸题,但我不会写……
学习并刷了两天的题,是时候来写个总结了

正文

树上启发式合并(DSU on Tree),是一个在 O ( n l o g n ) O(nlogn) 时间内解决许多树上问题的有力算法。
但它的中心其实是——暴力!
没错,它正是由暴力优化而来。
我们先看一道例题:CF600E Lomsat gelral
题意简述:一棵树有n个结点,每个结点都是一种颜色,每个颜色有一个编号,求树中每个子树的最多的颜色编号的和。
我们先思考暴力:对于每个节点,暴力遍历子树,将它们的数据统计出来得到当前节点的答案,然后再暴力将这棵子树的数据清空,以免影响到别的节点。
很明显,这个做法是 O ( n 2 ) O(n^2) 的。

有的同学可能会有疑问:为什么要清空呢?
   \;
由于空间的限制,我们不可能对于每一个节点开一个数组来记录数据,只能开一个全局数组。
在这个全局数组内,如果不清空,就会影响到别的子树,于是导致答案错误。
然而可以发现,统计儿子节点时最后那个节点其实没有必要清空,因为它不再会影响到它的兄弟节点。
这也正是接下来要讲到的优化方法。

思考优化:对于节点x,可以在做子树答案时保留最后一棵子树v的数据不清空,然后统计x的答案时绕过v节点统计别的子树。那么v选哪个呢?当然是选size最大的。
于是,我们得到了一个优化后的做法:对于节点x,先统计轻儿子的答案,并将它们的数据清除;然后统计重儿子的答案,保留数据;最后遍历其他轻儿子及其子树,把它们的数据与重儿子合并。
非常神奇的是,经过分析,可以证明它的复杂度是 O ( n l o g n ) O(nlogn) 的!(然而我不会证明,也懒得学)
回到例题,这正是可以用这种方法简单解决的。放代码:

bool s[sz];//是否是重儿子
int cnt[sz];//每种颜色出现次数
ll sum[sz],top;//每个次数之和,以及最多的次数
void add(int c,int t)
{
    sum[cnt[c]]-=c;//原来的减去
    cnt[c]+=t;
    sum[cnt[c]]+=c;//新的加上
    if (sum[top+1]) ++top;//更新最大值
    if (!sum[top]) --top;
}
void add(int x,int fa,int t)
{
    add(col[x],t);//更新数据
    go(x) if (v!=fa&&!s[v]/*绕过重儿子*/) add(v,x,t);
}
ll ans[sz];
void dfs(int x,int fa,bool keep)//keep:是否保留当前子树的数据
{
    go(x) if (v!=fa&&v!=son[x]) dfs(v,x,0);//遍历轻儿子,不保留数据
    if (son[x]) dfs(son[x],x,1);//遍历重儿子,保留数据
    s[son[x]]=1;//标记重儿子
    add(x,fa,1);//把轻儿子与重儿子的数据合并
    ans[x]=sum[top];
    s[son[x]]=0;//取消标记
    if (!keep) add(x,fa,-1);//若不保留则暴力删除数据(相当于memset,但memset太慢)
}

练习

经过上面的讲解,相信各位已经大概明白了树上启发式合并的思路。接下来还有几道练习题(地址均为洛谷题库,要去原OJ请通过洛谷上的链接过去):
CF570D Tree Requests
CF208E Blood Cousins
CF246E Blood Cousins Return
CF1009F Dominant Indices
CF375D Tree and Queries
CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
洛谷上均有对应的树上启发式合并题解,有一些是我的,可以点赞。(不要脸地骗个赞)
如果还不太明白,可以去这个博客看。(然而是英文的)

完结撒花!!

猜你喜欢

转载自blog.csdn.net/pb122401/article/details/84648993