【学习笔记】DSU on Tree
概述
DSU on Tree 即树上启发式合并,重点不在“合并”,而在利用树链剖分的性质对子树问题进行复杂度正确的分治。
面对多组询问但不带修改的树上题目,可以使用 DSU on Tree 解决。
算法流程
-
递归处理轻儿子的答案
-
递归处理重儿子的答案
-
重新遍历轻儿子子树,计算当前子树的答案
-
如果当前节点是轻儿子,重新遍历整棵子树,清除答案
发现一个节点被再次遍历当且仅当其所在的子树根为轻儿子,在这之后其所在子树大小至少扩大 \(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}\) 位置统计答案,即要求:
由于保证存在 \(10\) 的逆元,移项可以得到:
这样“拼数字”的意义就转成了模意义下的加减乘除。
简单做法是点分治分别维护。
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)\)。
得到两个标记的变化后可以轻松计算答案。