🔖 acm算法树链剖分

算法简介

树链剖分是将树上的节点映射到一段连续的区间中,使得树上任意一条路径能用不超过 O(logN)  段连续区间表示。这样,利用线段树就可以在 O(log2N) 的时间复杂度对树上任意路径上的节点进行进行维护和查询了。

算法原理

  • siz(u): 以 u 为根的子树的节点数
  • son(u): u 的子节点中 siz 值最大的节点
  • 重边: uson(u) 的连边
  • 轻边: u 与除 son(u) 外其它子节点的连边
  • 重链: 由重边连成的路径

定理1

如果 (u,v) 是一条轻边,那么 siz(v)<siz(u)2.

因为 (u,v) 是一条轻边,所以 u 还有一个由重边相连的儿子 son(u).
siz(v)siz(u)2,根据定义,有 siz(son(u))siz(v)siz(u)2.
siz(u)siz(v)+siz(son(u))+1>2×siz(v) 矛盾。
故,siz(v)<siz(u)2


定理2

任意非根节点 u 到根节点的路径上,轻边+重链 总数不超过 O(log2N).

不难证明,最多会遇到 log2N轻边
因为从根节点到 u 的路径中,每遇到一条轻边,节点个数就会减半,
所以轻边的数目不超过 log2N.
而整条路径上 log2N轻边最多隔开 log2N+1重链
故,轻边+重链 总数不超过 O(log2N)


定理2 可知当我们将一棵树沿着重链剖分后,将重链依次映射到一段连续的区间后,就可以将任何一条到根的链分成 logN 段连续区间了。也就是用 logN 条重链的覆盖这一条路径。

算法实现

  • fat(u): u 的父亲
  • son(u): u 的子节点中 siz 值最大的节点
  • dep(u): u 的深度,根节点深度为 1
  • siz(u): 以 u 为根的子树的节点数
  • pos(u): u 在连续区间的映射值
  • top(u): u 所在重链的顶端节点

  • 求出 siz,dep,son,fat

    dfs1.cpp 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int fat[MAXN], son[MAXN], dep[MAXN], siz[MAXN];
    void dfs(int o, int f, int d) {
    fat[o] = f; son[o] = 0;
    dep[o] = d; siz[o] = 1;
    for (int u = from[o]; u; u = nxt[u]) {
    int v = to[u];
    if (v == f) continue;
    dfs(v, o, d+1);
    siz[o] += siz[v];
    if (siz[son[o]] < siz[v]) son[o] = v;
    }
    }
  • 求出 top,pos

    dfs2.cpp 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    int top[MAXN], pos[MAXN], dfs_clock;
    void dfs(int u, int t) {
    pos[u] = ++dfs_clock;
    top[u] = t;
    if (!son[u]) return;
    // u 与 son(u) 在同一条重链上,故重链的顶端节点相同
    dfs(son[u], t);
    for (int i = from[u]; i; i = nxt[i]) {
    int v = to[i];
    // u 的其它非 son(u) 子节点为新的重链的顶端节点
    if (v != fat[u] && v != son[u]) dfs(v, v);
    }
    }

树链剖分完了!!怎么用呢?如下图所示,粗线表示重边,虚线表示轻边,黑色数字为节点编号,紫色数字为边编号。图中的 0 节点为一虚拟节点,引进它是方便理解下文 [1]。每条边上的紫色数字表示箭头所指的节点的 pos 值。

1.png

对于树上的两个节点 uv,若它们的最近公共祖先(LCA)为 w;那么,如果我们想要对路径 (u,v) 上的所有节点进行一个操作,只需要让 u,v 同时沿着各自的祖先节点走,并在 w 处相遇就可以了。

top 是为了加速往上走的过程;但是不难发现,由于每次可能不仅走一步,如果 uv 同时行动的话,很可能会错过 w !为了解决这个问题,可以总是让 dep(top) 值大的点先走(想一想,为什么)。

不妨假设 dep(top(u))dep(top(v))

  • top(u)top(v),则得到一段连续的映射区间 [pos(top(u)),pos(u)];让 u 走到 fat(top(u))
  • 否则 top(u)=top(v),即 uv 在同一条重链中。显然, w={x|dep(x)=min{dep(y)|y{u,v}},x{u,v}},得到一段连续的映射区间 [pos(w),pos({u,v}{w})]

由于每次走到 fat(top),可以放心的把轻边当做长度为 0重链来处理。比如,我们现在要访问 节点6 --> 节点15 的路径:

  1. u 走到 12,得到一段连续的映射区间 [7,7]
  2. u 走到 1,得到一段连续的映射区间 [2,3]
  3. v 走到 2,得到一段连续的映射区间 [12,12]
  4. v 走到 1,得到一段连续的映射区间 [11,11]
  5. top(u)=top(v),得到一段连续的映射区间 [1,1];相遇,终止算法

所以,我们在映射区间里依次对 {[7,7],[2,3],[12,12],[11,11],[1,1]} 进行操作就好了。

update.cpp 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void update(int L, int R) {
while (top[L] != top[R]) {
// 让 dep 大的走
if (dep[top[L]] < dep[top[R]]) swap(L, R);
// 得到连续映射区间 [l,r]
int l = pos[top[L]], r = pos[L];
// 对 [l,r] 进行操作
fun(l, r);
L = fat[top[L]];
}
if (dep[L] > dep[R]) swap(L, R);
fun(pos[L], pos[R])
}

小记

上文中讨论的是对 (u,v) 路径上的所有节点进行操作。若信息全维护在边上,即要对 (u,v) 路径上的所有边进行操作,则仅需在 dep(u)=dep(v) 时,执行的区间改成 [pos(son(u)),pos(v)] 就行了。

  •  [1]: 

    在实际实现中,我们令 fat( 根节点 )= 根节点,则上图的 0 节点指代 1

© 2017-2025 光和尘有花满渚、有酒盈瓯

Comments