【学习笔记】DSU on Tree

Page Views Count

概述#

DSU on Tree 即树上启发式合并,重点不在“合并”,而在利用树链剖分的性质对子树问题进行复杂度正确的分治。

面对多组询问但不带修改的树上题目,可以使用 DSU on Tree 解决。

算法流程#

  1. 递归处理轻儿子的答案

  2. 递归处理重儿子的答案

  3. 重新遍历轻儿子子树,计算当前子树的答案

  4. 如果当前节点是轻儿子,重新遍历整棵子树,清除答案

发现一个节点被再次遍历当且仅当其所在的子树根为轻儿子,在这之后其所在子树大小至少扩大 2 倍,因此每个节点最多被遍历 O(logn) 次,那么总遍历次数是 O(nlogn),若单个节点计算答案复杂度 O(k),总复杂度 O(knlogn)

实现如下:

void dfs1(){
    // 求出重儿子
}
void add(){
    // 加入一个节点的贡献
}
void del(){
    // 删去一个节点的贡献
}
void insert(int u){
    // 加入整棵子树的贡献
    add(u);
    for(int i=head[u],v;i;i=e[i].nxt){
        v=e[i].to;
        if(v==fa[u]) continue;
        insert(v);
    }
}
void erase(int u){
    // 删去整棵子树的贡献
    del(u);
    for(int i=head[u],v;i;i=e[i].nxt){
        v=e[i].to;
        if(v==fa[u]) continue;
        erase(v);
    }
}
void dfs2(int u){
    // 递归处理轻儿子答案
    for(int i=head[u],v;i;i=e[i].nxt){
        v=e[i].to;
        if(v==fa[u]||v==son[u]) continue;
        dfs2(v);
    }
    // 递归处理重儿子答案
    if(son[u]) dfs2(son[u]);
    // 重新遍历轻儿子子树,计算当前子树的答案
    add(u);
    for(int i=head[u],v;i;i=e[i].nxt){
        v=e[i].to;
        if(v==fa[u]||v==son[u]) continue;
        insert(v);
    }
    // 如果当前节点是轻儿子,重新遍历整棵子树,清除答案
    if(fa[u]&&son[fa[u]]!=u) erase(u);
}

例题#

CodeForces-600E Lomsat gelral *2300#

维护当前最多出现次数以及对应元素之和即可。

CodeForces-570D Tree Requests *2200#

一个判断方式是最多只有一种字符出现次数为奇数,那么字符权值设计成 2c,记录一下每个深度的异或和即可轻松判断。

CodeForces-208E Blood Cousins *2100#

树剖求出 k 级祖先,变成求子树内某深度节点个数。

CodeForces-375D Tree and Queries *2400#

朴素 DSU on Tree,后缀和使用树状数组,时间复杂度 O(nlog2n)

CodeForces-741D Arpa's letter-marked tree and Mehrdad's Dokhtar-kosh paths *2900#

比较困难。

考虑改成求 LCAu 的最长合法路径,把每个字符出现次数的奇偶设计成状态是平凡的,可以想到对每个状态维护最大深度,查询 O(|Σ|)

问题是增加一条边如何继承重儿子的信息,发现类似全局异或这条边的值,不妨维护一个标记 tag 表示当前全局异或的标记,这样查询和修改都是原数异或上 tag 进行。轻儿子合并以及暴力清空时,可以用前缀异或和 O(1) 计算到祖先路径上的异或和。

时间复杂度 O(|Σ|nlogn)

CodeForces-715C Digit Tree *2700#

S(u,v) 表示 (u,v) 路径上数字顺次拼接而成的数,D(u,v) 表示 (u,v) 路径上数字个数,那么在 LCA 位置统计答案,即要求:

S(u,LCA)×10D(LCA,v)+S(LCA,v)0(modm)

由于保证存在 10 的逆元,移项可以得到:

S(u,LCA)S(LCA,v)10D(LCA,v)(modm)

这样“拼数字”的意义就转成了模意义下的加减乘除。

简单做法是点分治分别维护。

DSU on Tree 做法比较复杂。

考虑用 map 分别维护 S(u,LCA) 以及 S(LCA,u)10D(LCA,u),第一次 DFS 可以预处理到根路径上数字正序倒序得到的数字,可以快速得到一段祖先关系的路径上数字拼出的结果。

重点是考虑继承重儿子,即加入一条边 (u,v,w) 影响,不妨维护 tagcnt,tagsum 表示当前实际值 Smap 中存储值 S 的关系为 S=S×10tagcnt+tagsum

对于维护 S(u,LCA)map,增加一条边使得 S=S×10+w,于是得到 S=S×10tagcnt+1+(tagsum×10+w),可以得到两个标记的变化。

对于维护 S(LCA,u)10D(LCA,u)map,发现维护的值实际上是把拼出来的数作为小数部分,那么增加一个 w 就是在个位增加再向前移动小数点,于是 S=110×(S+w),得到 S=S×10tagcnt1+110×(tagsum+w)

得到两个标记的变化后可以轻松计算答案。

参考资料#

作者:SoyTony

出处:https://www.cnblogs.com/SoyTony/p/Learning_Notes_about_DSU_on_Tree.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   SoyTony  阅读(132)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示