浅谈树上问题
树的直径
定义
规定树上任意两节点之间的最远距离为树的直径
解法
较为主流的解法有两种
-
贪心
以任意节点 \(x\) 为根进行一次 \(\text{DFS}\) ,记录距 \(x\) 最远的节点编号为 \(y\) ,再以 \(y\) 为根进行第二次 \(\text{DFS}\) ,得到距 \(y\) 最远的节点编号 \(z\) ,那么 \(dis(x,y)\) 即为树的直径。
其中利用了一个定理:在一棵树上,从任意节点 \(x\) 开始进行一次 DFS,到达的距离其最远的节点 \(y\) 必为直径的一端。
可惜的是该算法只在边权非负的时候成立,关于该做法的正确性可见 OI Wiki ,此处不再赘述。
-
dp
该种方法可以解决贪心解法无法处理的负权图。
首先需要明确一个结论,对于一个有根树,树的直径只有两种情况
-
直径的两个端点分别在根的两个子树当中
-
根是直径的一个端点
明确这件事以后我们就可以利用分治的思想求解了,我们只需要考虑端点不在同一个子树内的情况,在递归中将情况 \(2\) 转化为情况 \(1\) 求解。
定义 \(f_u\) 为以 \(u\) 为根的子树中的最长链, \(g_u\) 为以 \(u\) 为根的子树中的次长链(与最长链不相交),这样问题就被转化为了上面讨论的情况 \(1\) 了,即 \(\max\limits_{u\in Tree}\{f_u+g_u\}\) 即为树的直径。
-
动态直径
此处的动态指的是改变树上任意一边的边权(无论操作是否独立),该问题有很多种解法,例如: \(\text{ddp}\) 、\(\text{LCT}\) 、动态点分治等,但此处暂时只介绍线段树维护欧拉序的做法。
-
线段树维护欧拉序
此做法是正确性是基于边权非负的基础上的。
定义 \(dep_u\) 为点 \(u\) 到树根的距离,首先不难得到树上任意两点之间的距离 \(dis(u,v)\) 满足该式:
\[dis(u,v) = dep_u + dep_v - 2 \times dep_{LCA(u,v)} \]如果熟悉欧拉序,不难知道 \(\text{LCA}\) 可以被规约为 \(\text{RMQ}\) 问题,那么问题就被转化为求这样一个值
\[\max_\limits{1\leq l \leq r \leq 2\times n - 1}\{A_l + A_r - 2\times \min_\limits{l\leq i \leq r}A_i\} \]其中 \(A_i\) 为欧拉序为 \(i\) 的节点的深度
对于上式,我们可以考虑固定左边界,维护该式:
\[rmax_{[L,R]} = A_i - 2\times \min_\limits{i\leq j}\{A_j\}\quad(L\leq i \leq j \leq R) \]对应的,我们也需要固定右边界,维护这个式子:
\[lmax_{[L,R]} = A_i - 2 \times \min_\limits{j\leq i}\{A_j\}\quad (L\leq j \leq i\leq R) \]因此对于每一个线段树上的节点,我们需要维护如下信息:
\[\begin{cases} \text{mx} & 表示欧拉序位于该区间中的节点的最大深度 \\ \text{mn} & 表示欧拉序位于该区间中的节点的最小深度 \\ \text{dia} & 表示欧拉序位于该区间中的子树直径 \\ \text{lmx} & 上文所提到的信息 \\ \text{rmx} & 同上 \end{cases} \]同时也不难得到如何合并信息
struct info { i64 mx, mn, lmx, rmx, dia, tag; info() { mx = mn = lmx = rmx = tag = 0; } info operator+(const info& a) const { info now; now.mx = max(mx, a.mx); now.mn = min(mn, a.mn); now.lmx = max({lmx, a.lmx, mx - 2 * a.mn}); now.rmx = max({rmx, a.rmx, a.mx - 2 * mn}); now.dia = max({dia, a.dia, mx + a.rmx, lmx + a.mx}); return now; } };
对于线段树来说,我们只需要一个能实现区间加的线段树即可,此处不多赘述。
LCA
倍增
年轻人的第一种求 LCA 的算法!
记 \(fa_{i,x}\) 为 \(i\) 号节点的 \(2^{x}\) 级祖先,这个信息只需要一次 DFS 就可以统计出来。
然后开始倍增,利用 \(fa_{i, x - 1}\) 和 \(fa_{fa_{i, x - 1}, x - 1}\) 得到 \(fa_{i, x}\) (即当前节点 \(x - 1\) 级祖先的 \(x - 1\) 级祖先是当前节点的 \(x\) 级祖先),不难发现预处理所有 \(fa_{i, x}\) 的时间复杂度为 \(\Omicron(n\log n)\)
在求解 \(LCA(u,v)\) 时,我们需要先将深度较深的节点向上跳,使得 \(u\) 和 \(v\) 处在同一深度,若两点重合,那么该点即为二者的 LCA ,否则两节点需同时倍增向上跳直到两节点的父亲节点相同,此时他们的父亲节点就是 \(LCA(u,v)\) ,该部分时间复杂度为 \(\Omicron(\log n)\)
总时间复杂度为 \(\Omicron(n\log n) - \Omicron(\log n)\) ,空间复杂度为 $ \Omicron(n\log n)$
欧拉序
欧拉序从根节点出发到回到根节点为止,按 DFS 的顺序所经过的所有点的顺序,因此有结论,两个点在欧拉序中第一次出现的位置之间一定有它们的 LCA,且它们的 LCA 是这个区间中深度最小的那个点,因此 LCA 问题就可以被转化成为 RMQ 问题,如果用 ST 表维护深度,最终是可以做到时间复杂度 \(\Omicron(n\log n) - \Omicron(1)\) ,空间复杂度 $ \Omicron(n\log n) $ 的。
树链剖分
当 \(top_u \ne top_v\),且 \(dep_{top_u} \ge dep_{top_v}\) 时,使得 \(u\) 变为其所在重链链首的父亲,直到 \(top_u = top_v\) 后,两者深度较小的点即为 \(LCA(u,v)\) ,时间复杂度 \(\Omicron(n) - \Omicron(\log n)\) ,空间复杂度 $ \Omicron(n) $