引入

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。

具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。

树链剖分(树剖/链剖)有多种形式,如 重链剖分长链剖分 和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」)。大多数情况下(没有特别说明时),「树链剖分」都指「重链剖分」。

重链剖分可以将树上的任意一条路径划分成不超过 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。

重链剖分还能保证划分出的每条链上的结点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。例如:

  1. 修改 树上两点之间的路径上 所有点的值。
  2. 查询 树上两点之间的路径上 结点权值的 和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)

除了配合数据结构来维护树上路径信息,树剖还可以用来 (且常数较小)地求 LCA。在某些题目中,还可以利用其性质来灵活地运用树剖。

重链剖分

我们给出一些定义:

定义 重子结点 表示其子结点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子结点,就无重子结点。

定义 轻子结点 表示剩余的所有子结点。

从这个结点到重子结点的边为 重边

到其他轻子结点的边为 轻边

若干条首尾衔接的重边构成 重链

把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。

如图:

实现

树剖的实现分两个 DFS 的过程。伪代码如下:

第一个 DFS 记录每个结点的父结点()、深度()、子树大小()、重子结点()。

第二个 DFS 记录所在链的链顶(,应初始化为结点本身)、重边优先遍历时的 DFS 序()、DFS 序对应的结点编号()。

以下为代码实现。

我们先给出一些定义:

  • 表示结点 在树上的父亲。
  • 表示结点 在树上的深度。
  • 表示结点 的子树的结点个数。
  • 表示结点 重儿子
  • 表示结点 所在 重链 的顶部结点(深度最小)。
  • 表示结点 DFS 序,也是其在线段树中的编号。
  • 表示 DFS 序所对应的结点编号,有

我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 ,第二次 DFS 求出

void dfs1(int u, int f) {
  fa[u] = f, dep[u] = dep[f] + 1, siz[u] = 1;
  for (auto v : G[u]) {
    if (v == f) continue;
    dfs1(v, u);
    siz[u] += siz[v];
    if (siz[v] > siz[son[u]]) son[u] = v;
  }
}
 
void dfs2(int u, int ftop) {
  top[u] = ftop, dfn[u] = ++idx, rnk[idx] = u;
  if (son[u]) dfs2(son[u], ftop);
  for (auto v : G[u])
    if (v != son[u] && v != fa[u]) dfs2(v, v);
}

重链剖分的性质

树上每个结点都属于且仅属于一条重链

重链开头的结点不一定是重子结点(因为重边是对于每一个结点都有定义的)。

所有的重链将整棵树 完全剖分

在剖分时 重边优先遍历,最后树的 DFS 序上,重链内的 DFS 序是连续的。按 DFN 排序后的序列即为剖分后的链。

一棵子树内的 DFS 序是连续的。

可以发现,当我们向下经过一条 轻边 时,所在子树的大小至少会除以二。

因此,对于树上的任意一条路径,把它拆分成从 LCA 分别向两边往下走,分别最多走 次,因此,树上的每条路径都可以被拆分成不超过 条重链。

常见应用

路径上维护

用树链剖分求树上两点路径权值和,伪代码如下:

链上的 DFS 序是连续的,可以使用线段树、树状数组维护。

每次选择深度较大的链往上跳,直到两点在同一条链上。

同样的跳链结构适用于维护、统计路径上的其他信息。

子树维护

有时会要求,维护子树上的信息,譬如将以 为根的子树的所有结点的权值增加

在 DFS 搜索的时候,子树中的结点的 DFS 序是连续的。

每一个结点记录 bottom 表示所在子树连续区间末端的结点。

这样就把子树信息转化为连续的一段区间信息。

求最近公共祖先

不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA。

向上跳重链时需要先跳所在重链顶端深度较大的那个。

参考代码:

int lca(int u, int v) {
  while (top[u] != top[v]) {
    if (dep[top[u]] > dep[top[v]])
      u = fa[top[u]];
    else
      v = fa[top[v]];
  }
  return dep[u] > dep[v] ? v : u;
}

换根操作

考虑一类新的问题:除了树链剖分支持的基本操作外,加上了换根操作。

由于树链剖分维护的信息是静态的,不支持动态修改。同时,不可能每次换根后重新预处理信息,复杂度过高。那么,需要充分利用之前得到的信息来帮助解决换根操作。

对于路径修改和查询操作,由于树上两点之间的简单路径唯一,所以不会发生变化,与正常的处理方式相同。

对于子树修改和查询操作,一般的思路就是将换根后的子树映射到原来的子树。这需要分类讨论操作子树的根结点、换根后的整个树的根结点,以及原来树的根结点的相对位置关系。具体细节详见 后文例题

例题

本文通过例题展示如何应用重链剖分。首先是一道模板题。

对一棵有 个结点,结点带权值的静态树,进行三种操作共 次:

  1. 修改单个结点的权值;
  2. 查询 的路径上的最大权值;
  3. 查询 的路径上的权值之和。

保证

然后是一道带换根操作的重链剖分模板题。

给定一棵 个结点的树(初始根结点为 ),要求支持如下的 次操作:

  • 换根,将结点 设为新的树根。
  • 修改路径上结点权值,将结点 和结点 之间路径上的所有结点(包括这两个结点)的权值增加
  • 修改子树上结点权值,将以结点 为根的子树上的所有结点的权值增加
  • 询问路径,询问结点 和结点 之间路径上的所有结点(包括这两个结点)的权值和。
  • 询问子树,询问以结点 为根的子树上的所有结点的权值和。

最后是一道交互题,也是树剖的非传统应用。

有一棵以 为根的二叉树,你可以询问任意两点之间的距离,求出每个点的父亲。

结点数不超过 ,你最多可以进行 次询问。

长链剖分

长链剖分本质上就是另外一种链剖分方式。

定义 重子结点 表示其子结点中子树深度最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子结点,就无重子结点。

定义 轻子结点 表示剩余的子结点。

从这个结点到重子结点的边为 重边

到其他轻子结点的边为 轻边

若干条首尾衔接的重边构成 重链

把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。

如图(这种剖分方式既可以看成重链剖分也可以看成长链剖分):

长链剖分实现方式和重链剖分类似,这里就不再展开。

常见应用

首先,我们发现长链剖分从一个结点到根的路径的轻边切换条数是 级别的。

长链剖分优化 DP

一般情况下可以使用长链剖分来优化的 DP 会有一维状态为深度维。

我们可以考虑使用长链剖分优化树上 DP。

具体的,我们每个结点的状态直接继承其重儿子的结点状态,同时将轻儿子的 DP 状态暴力合并。

给定一棵有 个顶点的有根树,以顶点 作为根。

定义顶点 的深度数组为一个无限序列 ,其中 表示满足以下两个条件的顶点 的数量:

  • 的祖先;
  • 的简单路径恰好经过 条边。

顶点 的深度数组的主导下标(dominant index)(简称顶点 的主导下标)定义为一个下标 ,满足:

  • 对于所有 ,都有
  • 对于所有 ,都有

请计算树中每个顶点的主导下标。

注意,一般情况下 DP 数组的内存分配为一条重链整体分配内存,链上不同的结点有不同的首位置指针。

DP 数组的长度我们可以根据子树最深结点算出。

当然长链剖分优化 DP 技巧非常多,包括但是不仅限于打标记等等。这里不再展开。

参考 租酥雨的博客

长链剖分求 k 级祖先

即询问一个点向父亲跳 次跳到的结点。

首先我们假设我们已经预处理了每一个结点的 级祖先。

现在我们假设我们找到了询问结点的 级祖先满足

我们考虑求出其所在重链的结点并且按照深度列入表格。假设重链长度为

同时我们在预处理的时候找到每条重链的根结点的 级祖先,同样放入表格。

根据长链剖分的性质,, 也就是说,我们可以 在这条重链的表格上求出的这个结点的 级祖先。

预处理需要倍增出 次级祖先,同时需要预处理每条重链对应的表格。

预处理复杂度 , 询问复杂度

习题