首页 > 技术文章 > 『笔记』树链剖分

Frather 2021-03-13 14:14 原文

前置

别说你不知道什么是树。。。不知道就去睡觉吧。

教你百度?

DFN 序

  • 记录 \(dfs\) 的时候每个点访问起始时间与结束时间,记录起始时间是前序遍历,结束时间是后序遍历。
    可以轻易地发现树的一棵子树在 \(dfs\) 序上是连续的一段。

  • \(dfn\) 序是节点按照 \(dfs\) 进入节点的顺序排列的序列。
    一般 \(dfn\) 序可以认为是 \(dfs\) 序的一半、是 \(dfs\) 序的子序列 。

一些可能用到的概念

  • 重儿子

    • 在所有的节点中拥有子树节点最多的一个父节点

    • 是个节点

  • 轻儿子

    • 除去重儿子其它所有的父节点

    • 也是个节点

  • 重边

    • 父节点和重儿子连成的边

    • 是一条边

  • 轻边

    • 父节点和轻儿子连成的边

    • 也是一条边

  • 重链

    • 由多条重边连接而成的路径
  • 轻边

    • 有多条轻边链接而成的路径

对于上图:

1 号节点的子结点中,4 号节点的子树节点比 2、3 号的多,所以 4号节点是重儿子;

4 号节点的子节点中,9 号节点的子树节点比 8、10 号的多,所以 9号节点是重儿子;

以此类推。

那么:

图中加粗的节点都是重儿子,其它节点都是轻儿子;

红色边都是重边,黑色边都是轻边;

\(1 \to 4 \to 9 \to 12 \to 14\)\(3 \to 7\)\(2 \to 6 \to 11\) 是重链。

来张更详细具体的图片:


图片来自 ChinHhh's blog

定义

百度者云:

一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、BST、SPLAY、线段树等)来维护每一条链。

简单来说就是对树进行轻重边剖分,分割成多条链,然后通过某些数据结构进行维护。

其实本质上来说还是一种优化暴力。

算法的本质不就是加速枚举么

or


by attack

功能实现

对于上述两张图片的分析,可以发现一个规律:

叶节点没有重儿子,非叶节点有且只有一个重儿子,并不是轻儿子之后全是轻儿子。

或者换句话说:

当一个节点选了他的重儿子之后,我们并不能保证它的轻儿子就是叶节点,所以我们就以这个轻儿子为根,再去选这个轻儿子的轻重儿子。

其实这就是一个 \(dfs\) 过程,而且通过上述操作,我们可以得到许多重链。

变量声明

int n, m;

const int _ = 100010;

struct edge
{
    int to;
    int nxt;
} e[2 * _];
int cnt, head[_];

struct Node
{
    int sum;
    int lazy;
    int l;
    int r;
    int ls;
    int rs;
} node[2 * _];

int fa[_], dep[_], siz[_], mson[_], rk[_], top[_], id[_];
名称 含义
fa[u] 节点 u 的父亲节点
dep[u] 节点 u 的深度
siz[u] 以节点 u 为根的子树节点数目
mson[u] max_weight_son 重儿子
rk[u] 当前 dfs 标号在树中所对应的节点
top[u] 当前节点所在链的顶端节点
id[i] 节点 i 在树链剖分后的新编号( dfs 序)

实现思路

通过两次 \(dfs\) 实现。

dfs1()

对于某个节点:

  1. 求出它所在的子树大小

  2. 找到该子树的重儿子

(处理出 siz 和 mson 数组)

  1. 记录其父亲以及深度

(处理出 f , d 数组)

/*

代码来自网络。

*/
void dfs1(int u, int fa, int depth) //当前节点、父节点、层次深度
{
    fa[u] = fa_;
    d[u] = depth;
    siz[u] = 1; //对于这个点本身来说siz=1
    for (int i = head[u]; i; i = e[i].nxt)
    {
        int v = e[i].to;
        if (v == fa)
            continue;
        dfs1(v, u, depth + 1); //层次深度+1
        siz[u] += siz[v];    //子节点的siz已被处理,用它来更新父节点的siz
        if (siz[v] > siz[mson[u]])
            mson[u] = v; //选取siz最大的作为重儿子
    }
}
//进入
dfs1(root, 0, 1);

图片来自 Ivanovcraft's blog

dfs2()

  1. 链接重链

  2. 标记每个节点的 \(dfs\)

  3. 用数据结构维护重链,保证每一条重链上各个节点 \(dfs\) 序上是连续的一段

(处理出 top 、 id 、rk 数组)

/*

代码来自网络。

*/
void dfs2(int u, int t) //当前节点、重链顶端
{
    top[u] = t;
    id[u] = ++cnt; //标记dfs序
    rk[cnt] = u;   //序号cnt对应节点u
    if (!mson[u])
        return;
    dfs2(mson[u], t);
    /*我们选择优先进入重儿子来保证一条重链上各个节点dfs序连续,
一个点和它的重儿子处于同一条重链,所以重儿子所在重链的顶端还是t*/
    for (int i = head[u]; i; i = e[i].nxt)
    {
        int v = e[i].to;
        if (v != mson[u] && v != fa[u])
            dfs2(v, v); //一个点位于轻链底端,那么它的top必然是它本身
    }
}
图片来自 Ivanovcraft's blog

时间复杂度

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

  • 从根结点到任意结点的路所经过的轻重链的个数必定都小于 \(\log n\)

总体时间复杂度为:

\(\huge O(n \log ^ 2 n)\)

推荐阅读