0%

笔记 一些树上问题

单点、链、子树的查询及修改,很多时候可以相互转化;并且放到 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\) 的祖先关系分类讨论一下。

      具体应用见 题解 SP10707 COT2 - Count on a tree II

欧拉序

入栈时加入 \(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\) 的祖先)。


感觉还有很多要总结的。。。