Loading

【学习笔记】DSU on Tree

Page Views Count

概述

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

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

算法流程

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

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

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

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

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

实现如下:

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

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

CodeForces-208E Blood Cousins *2100

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

CodeForces-375D Tree and Queries *2400

朴素 DSU on Tree,后缀和使用树状数组,时间复杂度 \(O(n\log^2 n)\)

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

比较困难。

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

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

时间复杂度 \(O(|\Sigma|n\log n)\)

CodeForces-715C Digit Tree *2700

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

\[S(u,\mathrm{LCA})\times 10^{D(\mathrm{LCA},v)}+S(\mathrm{LCA},v)\equiv 0\pmod m \]

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

\[S(u,\mathrm{LCA})\equiv -\dfrac{S(\mathrm{LCA},v)}{10^{D(\mathrm{LCA},v)}}\pmod m \]

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

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

DSU on Tree 做法比较复杂。

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

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

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

对于维护 \(\frac{S(\mathrm{LCA},u)}{10^{D(\mathrm{LCA},u)}}\)map,发现维护的值实际上是把拼出来的数作为小数部分,那么增加一个 \(w\) 就是在个位增加再向前移动小数点,于是 \(S=\frac{1}{10}\times (S'+w)\),得到 \(S=S'\times 10^{tagcnt-1}+\frac{1}{10}\times (tagsum+w)\)

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

参考资料

posted @ 2023-08-22 08:56  SoyTony  阅读(80)  评论(0编辑  收藏  举报