树上启发式合并 - 个人总结

树上启发式合并(dsu on tree)用来处理这样一类题目:询问支持离线,并且询问与子树有关。它可以很方便地在O(nlogn) 内完成答案的统计。
我们基于这样一个简单的问题来讨论dsu on tree。U41492 树上数颜色 - 洛谷

给定一棵节点具有颜色的树,询问每棵子树中有多少种不同的颜色

(1)树上莫队:这个十分简单,我们不多赘述。时间复杂度为O(nn)

以下介绍两种启发式合并的方法:

(2)树上桶合并:我们把每个节点子树中的颜色编号存于一个set桶中,显然我们只需获取桶的大小即可(我不确定size()函数是否会导致复杂度升级,最好还是维护sz数组)。在桶往上传时,我们总是把小桶合并到大桶,于是可以保证每个元素被移出桶的次数最多只有logn。这样做的复杂度为O(nlogn),考虑到set的操作是logn的,于是总复杂度为O(nlogn2),能勉强应付1e6的数据。

(补充证明:对较小桶而言,合并后的新桶大小至少为自己的两倍,换而言之,对一个元素执行取出和压入操作,新桶大小必然大于等于旧桶的两倍,这意味着一个元素最多被执行logn次该操作。)

(3)树上启发式合并:

先论暴力算法。我们维护一个计数数组cnt,记录某颜色当前的数量,当cnt[x]由0变1时++ans。对每个节点,重置ans和cnt,然后遍历以它为根的子树,最后把ans记录在该点上。

而树上启发式合并基于的就是对O(n2)的优化。对于一个结点u,我们不一定非要在被清空的cnt和ans上重新开始,因为u的儿子子树的信息同样是自己的一部分,所以可以在其某个儿子遗留的未重置的cnt和ans上接着维护ans。我们指定这个儿子为重儿子,能使总复杂度达到O(nlogn)。

证明:每个节点除了最原始的遍历,在它到根节点的路径上,每有一条轻边就意味着要被多遍历一次。由树链剖分的引理可知,从根到任一节点的链上最多只有logn条轻边,于是每个节点最多被遍历logn+1次。

算法步骤:

1 预处理每个点的重儿子

2-1 进入dfs函数,函数首先向非重儿子方向递归,完成这些节点的处理,这主要是为了自下而上出结果。dfs函数默认不清除数据,因此要手动清除。

2-2 接着才正式开始启发式合并。具体见如下参考模板。

参考模板:

void dfs0( int u, int fa ) { //遍历整个树,找每个点的重儿子
siz[u] = 1;
for( int v : G[u]) {
if( v == fa ) continue;
dfs0( v, u );
siz[u] += siz[v];
if( siz[v] > siz[son[u]] )
son[u] = v;
}
}
int ans; //记录答案的全局变量,跟随全局数组一起重置
void calc( int u, int fa, bool flag ) {//遍历以u为根的子树,处理每个节点的数据
if( flag ) { //flag=0回退 flag=1添加
//统计数据
}
else {
//按原路清理数据(通常是彻底清除,直接赋0、inf或-inf)
}
for( int v : G[u] )
if( v != fa ) calc( v, u, flag );
}
void dfs( int u, int fa ) { //默认不清理遗留数据
for( int v : G[u] ) {
if( v == fa || v == son[u] ) continue;
dfs( v, u );//先算轻儿子的答案
calc( v, u, 0 );//计算完轻儿子的答案后 要把儿子的痕迹擦干净 为下一个儿子准备
ans=0;//或+-inf
//...可能还有其他的操作
} //以下才是启发式合并的正式开始
if( son[u] ) dfs( son[u], u );//重儿子的贡献仍然保留 不回退
//别忘了把u本身加入数据
//如果重儿子遗留的ans不是我们需要的,还得重置一下
for( int v : G[u] ) {
if( v == fa || v == son[u] ) continue;
calc( v, u, 1 );//开始重新添加每个轻儿子的贡献 为后面计算自己准备
}
//...一堆操作,比如将ans计入res[u],或者处理挂在u上的询问
}

个人总结:

(1)保证每个节点只被dfs一次,被calc不超过logn次

(2)清除数据不能用memset,而按其怎么加进来去原路把数据清除,这样防止了复杂度退化。

(3)calc函数的功能是遍历子树记录子孙节点数据,如果这些节点的数据是给定的或者有办法预处理,还可以通过提前生成dfs序,来代替遍历子树。

(4)树上启发式合并的典型运用Problem - D - Codeforces神题,多细品)。 以0/1表示22个字母分别出现奇数次还是偶数次,我们所维护的数组叫做len,len[s]表示从根到某一节点形成的01串s的最大深度(启发:节点记录的数据要么与本点绑定,要么就相对于根节点),节点的s和深度都是可以预处理好的(这样刚好能由两点01串的异或得到两点的路径信息)。对于当前节点u,我们的目标是找到经过u的最长合法路径。对u的每棵轻儿子子树,要进行三次calc,第一次是遍历子树中的节点x,从现有的len集合中找到所有与x形成合法匹配的01串s,用dep[x]+len[s]-2*dep[u]来更新ans,第二次是把子树中所有节点x的01串s更新到len集合,也就是len[s]=max(len[s],dep[x])。第三次是清空len,注意要重置为-inf而不是0。这道题目显然无法采用莫队,因为第一次calc和第二次calc是严格分开的。

posted @   ice_dragon_grass  阅读(118)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示