树上启发式合并 - 个人总结
树上启发式合并(dsu on tree)用来处理这样一类题目:询问支持离线,并且询问与子树有关。它可以很方便地在O(nlogn) 内完成答案的统计。
我们基于这样一个简单的问题来讨论dsu on tree。U41492 树上数颜色 - 洛谷
给定一棵节点具有颜色的树,询问每棵子树中有多少种不同的颜色
(1)树上莫队:这个十分简单,我们不多赘述。时间复杂度为
以下介绍两种启发式合并的方法:
(2)树上桶合并:我们把每个节点子树中的颜色编号存于一个set桶中,显然我们只需获取桶的大小即可(我不确定size()函数是否会导致复杂度升级,最好还是维护sz数组)。在桶往上传时,我们总是把小桶合并到大桶,于是可以保证每个元素被移出桶的次数最多只有logn。这样做的复杂度为
(补充证明:对较小桶而言,合并后的新桶大小至少为自己的两倍,换而言之,对一个元素执行取出和压入操作,新桶大小必然大于等于旧桶的两倍,这意味着一个元素最多被执行logn次该操作。)
(3)树上启发式合并:
先论暴力算法。我们维护一个计数数组cnt,记录某颜色当前的数量,当cnt[x]由0变1时++ans。对每个节点,重置ans和cnt,然后遍历以它为根的子树,最后把ans记录在该点上。
而树上启发式合并基于的就是对
证明:每个节点除了最原始的遍历,在它到根节点的路径上,每有一条轻边就意味着要被多遍历一次。由树链剖分的引理可知,从根到任一节点的链上最多只有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是严格分开的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App