浅谈树上问题

树的直径

定义

规定树上任意两节点之间的最远距离为树的直径

解法

较为主流的解法有两种

  1. 贪心

    以任意节点 \(x\) 为根进行一次 \(\text{DFS}\) ,记录距 \(x\) 最远的节点编号为 \(y\) ,再以 \(y\) 为根进行第二次 \(\text{DFS}\) ,得到距 \(y\) 最远的节点编号 \(z\) ,那么 \(dis(x,y)\) 即为树的直径。

    其中利用了一个定理:在一棵树上,从任意节点 \(x\) 开始进行一次 DFS,到达的距离其最远的节点 \(y\) 必为直径的一端。

    可惜的是该算法只在边权非负的时候成立,关于该做法的正确性可见 OI Wiki ,此处不再赘述。

  2. dp

    该种方法可以解决贪心解法无法处理的负权图。

    首先需要明确一个结论,对于一个有根树,树的直径只有两种情况

    • 直径的两个端点分别在根的两个子树当中

    • 根是直径的一个端点

    明确这件事以后我们就可以利用分治的思想求解了,我们只需要考虑端点不在同一个子树内的情况,在递归中将情况 \(2\) 转化为情况 \(1\) 求解。

    定义 \(f_u\) 为以 \(u\) 为根的子树中的最长链, \(g_u\) 为以 \(u\) 为根的子树中的次长链(与最长链不相交),这样问题就被转化为了上面讨论的情况 \(1\) 了,即 \(\max\limits_{u\in Tree}\{f_u+g_u\}\) 即为树的直径。

动态直径

此处的动态指的是改变树上任意一边的边权(无论操作是否独立),该问题有很多种解法,例如: \(\text{ddp}\)\(\text{LCT}\) 、动态点分治等,但此处暂时只介绍线段树维护欧拉序的做法。

  1. 线段树维护欧拉序

    此做法是正确性是基于边权非负的基础上的。

    定义 \(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) $

DSU On Tree

点分治

虚树

笛卡尔树

posted @ 2023-07-14 22:36  Jadebo1  阅读(29)  评论(0编辑  收藏  举报