单点、链、子树的查询及修改,很多时候可以相互转化;并且放到 dfs 序或者括号序上,从而避免重链剖分或者 LCT。
大量口胡警告⚠️
参照博客:
dfs 序 & 括号序 & 欧拉序
dfs 序
按照 dfs 的入栈顺序转化成一个序列。
性质:
树上一个子树在 dfs 序中是个连续的区间。
子树修改转化为序列问题。
树上一点与第一个访问的儿子在 dfs 序上是相邻的。
若优先 dfs 最大的儿子 / 最深的儿子,可用于重链剖分 / 长链剖分。
毕竟是 dfs 序,有时候可以模拟递归 dfs。
括号序
入栈加入左括号和编号,出栈加入右括号,位置分别记为 \(st_u\),\(pos_u\),\(ed_u\)。
性质:
若 \(st_u<st_v\) 且 \(ed_u>ed_v\),则 \(u\) 是 \(v\) 的祖先。
括号序上区间 \((pos_u,pos_v)\) 中未匹配的括号的个数就是 \(u-v\) 的树上距离 / 路径长度。
若题目满足答案可减性,那么树上的一条链对应括号序上的一段连续区间,那么可以把路径问题放到括号序上:
把 \(u\) 的答案在 \(st_u\) 处存 \(+1\) 倍,\(ed_u\) 处存 \(-1\) 倍,那么 \([1,st_u]\) 的和就是根到 \(u\) 路径的答案之和。可以树上差分得到链答案。
另一种是用于树上莫队,括号序记录点的编号,我们只统计区间内出现次数为 \(1\) 的点的答案,需要对 \(u,v\) 的祖先关系分类讨论一下。
欧拉序
入栈时加入 \(u\),出栈时加入 \(fa_u\)。记 \(u\) 在欧拉序中第一次出现位置 \(fir_u\)。
性质:
欧拉序 \([fir_u,fir_v]\) 中深度最小的点为 \(\operatorname{LCA}(u,v)\)。
通过 st 表可以 \(\mathcal O(n\log n)\) 预处理、\(\mathcal O(1)\) 求 \(\operatorname{LCA}\),看 这里。
树上差分
链上修改,设 \(lca\) 为 \(,\operatorname{LCA}(u,v)\)对于任意一条树上的链 \([u,v]\),都可以拆成两条“直上直下”的链,即 \([u,lca]\) 和 \([v,lca)\)。
然后就在 \(u,v\) 处标记 \(+1\),在 \(lca,fa_{lca}\) 处标记 \(-1\)。
用什么东西维护根到某一点的链和,或者像多次修改一次询问的题(松鼠的新家)最后处理。
链上查询,拆成两条链后再差分,因为我们往往容易得到根到某节点的路径和。下文默认省略拆链和差分。
一些转化关系
以下均只适用于能够简单加和的答案。
子树查询就是用数据结构维护 dfs 序。
链上修改,单点查询 \(\large\Rightarrow\) 单点修改,子树查询
差分链,一个点 \(u\) 为根的子树之和只会统计起点在子树里、终点是 \(u\) 的祖先的所有链的答案,而终点也在子树内的会抵消掉。所以转化为单点修改,子树查询。
右边到左边也是可以的,就是将单点修改转化为将根到该点的路径都修改了,不过没用。单点修改,链上查询 \(\large\Rightarrow\) 子树修改,单点查询
修改询问倒过来也一样,画个图模拟就懂了。实现也是维护根到该点的路径和,然后链底减去链顶就是链的答案。
链上修改,子树查询 \(\large\Rightarrow\) 单点修改,子树查询
链上修改对于子树的答案是多个,设 \(deep_u\) 表示 \(u\) 的深度,对于每个点维护修改值 \(val_u\) 和 \(val_u\times deep_u\)。这样子树查询就是 \(\sum val_v\times deep_v-deep_u\times\sum val_v\)(\(v\) 为 \(u\) 子树里的点)。
子树修改,链上查询 \(\large\Rightarrow\) 子树修改,单点查询
类似地,对 \(u\) 的子树修改值为 \(val_u\) 和 \(val_u\times deep_u\),单点 \(v\) 的答案得到根到 \(v\) 的路径和,即 \(-\sum val_u\times deep_u+deep_v\times \sum val_u\)(这里 \(u\) 为 \(v\) 的祖先)。
感觉还有很多要总结的。。。