重链剖分
创建时间:2025-04-30
树链剖分简介
树链剖分是一种解决树上路径/子树的修改/查询操作的算法,它的核心思想是将树化为若干条链,将路径/子树转化为序列,用线段树/分块/平衡树等数据结构维护。
树链剖分有重链剖分、长链剖分、实链剖分等实现,其中最常见的写法就是重链剖分(Heavy-Light Decomposition, LLD)。
重链剖分
首先需要维护数组 \(fa,dep,siz,son\) 分别表示每个节点的父亲、深度、子树大小、重儿子编号,其中重儿子的定义为 \(u\) 的儿子 \(v\) 中 \(siz[v]\) 最大的一个,不为重儿子的节点称为轻儿子。
显然,这几个数组可以通过一次 dfs 求出来,例如下面这段代码:
void dfs1(int u, int father, int depth) {
fa[u] = father;
dep[u] = depth;
siz[u] = 1;
for (int v : e[u]) {
if (v == father)
continue;
dfs1(v, u, depth + 1);
siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
我们将每个节点的重儿子和自己归为同一条链,这样,任何一条链都以一个轻儿子开头,且其它节点都是重儿子(这里将根节点也视为轻儿子)。接下来,进行第二次 dfs 维护数组 \(top,dfn,rev\) 分别表示 \(u\) 所在的链的第一个节点、\(u\) 的 dfs 序、dfs 序为 \(i\) 的节点的编号。值得注意的是,若此处先递归轻儿子,则同一条联上的节点的 \(dfn\) 就不是连续的了,这并不方便后续对树链的处理,故然这里优先递归 \(son[u]\)。
示例代码:
void dfs2(int u, int topf) {
dfn[u] = ++tot;
rev[tot] = u;
top[u] = topf;
if (son[u])
dfs2(son[u], topf);
for (int v : e[u])
if (!dfn[v])
dfs2(v, v);
}
重链剖分求解LCA
重链剖分最经典的应用就是求解 LCA 且常数很小,一般情况下树剖求解 LCA 比倍增快一些。
考虑朴素做法求 \(x\) 和 \(y\) 的 LCA,我们只需要不断将 \(x\) 和 \(y\) 往上跳,当 \(x=y\) 时便找到了 LCA,即此时的 \(x\)(\(y\))。为了不让 \(x\) 或 \(y\) 跳过头,我们肯定优先跳深度大的。示例代码:
int LCA(int x, int y) {
while (x != y) {
if (dep[x] < dep[y])
swap(x, y);
x = fa[x];
}
return x;
}
这个做法的复杂度为 \(O(n)\),还可以改进,下面使用树剖为上述做法加速。
首先,如果 \(x\) 和 \(y\) 在同一条链上,我们只需要比较它们的深度,深度小的就是 LCA。那如果 \(x\) 和 \(y\) 不在一条链上呢?如果按照朴素做法,此时需要将 \(x\) 和 \(y\) 中深度大的点往上跳,但显然,在这个前提下 \(x\) 和 \(y\) 的 LCA 肯定不在 \(x\) 和 \(y\) 其中一点当前所在的链上(注意,LCA 是有可能在链头深度小的点的链上的),所以 \(x\) 和 \(y\) 完全没必要都停留在当前链,应当将链头深度大直接向上跳过这条链,一步跳至链头的父节点。只要重复这个过程,\(x\) 和 \(y\) 终将到同一条链上,此时只需要按先前的讨论返回即可。
示例代码:
int LCA(int x, int y) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]])
swap(x, y);
x = fa[top[x]];
}
return dep[x] < dep[y] ? y : x;
}
这个算法的复杂度如何呢?由重儿子的定义知:一个轻儿子的 \(siz\) 一定不超过重儿子 \(siz\),而存在轻儿子时至少有两个儿子(一个重儿子一个轻儿子),故轻儿子的 \(siz\) 一定不超过 \(\frac {siz[u]} 2\),即 \(siz[u] \ge 2 \cdot siz[v]\)。又因为根的 \(siz=n\),所以 \(x\) 最多只会跳 \(\log n\) 次,\(y\) 也只会跳 \(\log n\) 次,故算法的复杂度为 \(O(\log n)\)。
重链剖分的基础应用
重链剖分经常与线段树相结合,以P3384 【模板】重链剖分/重链剖分为例,题目要求维护一个有 \(N\) 个节点、根为 \(R\) 的带点权的树,其中 \(i\) 的点权为 \(a[i]\),并支持 \(M\) 次操作:
- 将 \(x\) 到 \(y\) 路径上的所有节点权值加上 \(z\);
- 查询 \(x\) 到 \(y\) 路径上的点权之和对 \(P\) 取模;
- 将以 \(x\) 为根的子树中所有点的权值加上 \(z\);
- 查询以 \(x\) 为根的子树中所有点权之和。
我们可以线使用两次 dfs 将树剖成链,得到一个 \(a[rev[i]]\) 序列,并使用线段树维护这个序列。
对于操作1,类比求解 LCA 的过程,将 \(top\) 深度大的往上跳,但在 \(x \leftarrow fa[top[x]]\) 前应当先对 \(top[x] \sim x\) 这条链进行修改操作,即调用线段树的 \(\textup{add}(1, 1, N, dfn[top[x]], dfn[x], z)\)。当 \(top[x]=top[y]\) 时,不妨令 \(dep[x]>dep[y]\),调用 \(\textup{add}(1, 1, N, dfn[y], dfn[x], z)\);否则,交换 \(x,y\) 便和上述情况同理。
同样的,对于操作2,令 \(res\) 为答案,\(res \leftarrow res+\textup{query}(1, 1, N, dfn[top[x]], dfn[x])\),并 \(x \leftarrow fa[top[x]]\)。最后跳到同一条链上后,\(res \leftarrow res+\textup{query}(1, 1, N, dfn[y], dfn[x])\)。
考虑如何实现操作3和4,由树上 dfs 的性质知:不论怎样 dfs,一个以 \(u\) 为根的子树的 \(dfn\) 一定是连续的,且为 \([dfn[u], dfn[u]+siz[u]-1]\)!故操作3只需要 \(\textup{add}(1, 1, n, dfn[u], dfn[u]+siz[u]-1,z)\),操作4只需要输出 \(\textup{query}(1, 1, N, dfn[u]+siz[u]-1)\) 即可。
完整代码如下:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAX_N = 1e5 + 50;
int N, M, R, P, a[MAX_N], fa[MAX_N], tot;
int dep[MAX_N], siz[MAX_N], son[MAX_N];
int top[MAX_N], dfn[MAX_N], rev[MAX_N];
vector<int> e[MAX_N];
void dfs1(int u, int father, int depth) {
fa[u] = father;
dep[u] = depth;
siz[u] = 1;
for (int v : e[u]) {
if (v == father)
continue;
dfs1(v, u, depth + 1);
siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
void dfs2(int u, int topf) {
dfn[u] = ++tot;
rev[tot] = u;
top[u] = topf;
if (son[u])
dfs2(son[u], topf);
for (int v : e[u])
if (!dfn[v])
dfs2(v, v);
}
int sum[MAX_N << 2], tag[MAX_N << 2];
#define ls(cur) cur << 1
#define rs(cur) cur << 1 | 1
inline void pushup(int cur) {
sum[cur] = (sum[ls(cur)] + sum[rs(cur)]) % P;
}
inline void mark(int cur, int l, int r, int val) {
sum[cur] = (sum[cur] + 1ll * val * (r - l + 1)) % P;
tag[cur] = (tag[cur] + val) % P;
}
inline void pushdown(int cur, int l, int r) {
if (tag[cur]) {
int mid = l + r >> 1;
mark(ls(cur), l, mid, tag[cur]);
mark(rs(cur), mid + 1, r, tag[cur]);
tag[cur] = 0;
}
}
void build(int cur, int l, int r) {
if (l == r) {
sum[cur] = a[rev[l]] % P;
return ;
}
int mid = l + r >> 1;
build(ls(cur), l, mid);
build(rs(cur), mid + 1, r);
pushup(cur);
}
void add(int cur, int l, int r, int L, int R, int val) {
if (L <= l && r <= R) {
mark(cur, l, r, val);
return ;
}
pushdown(cur, l, r);
int mid = l + r >> 1;
if (L <= mid)
add(ls(cur), l, mid, L, R, val);
if (mid + 1 <= R)
add(rs(cur), mid + 1, r, L, R, val);
pushup(cur);
}
int query(int cur, int l, int r, int L, int R) {
if (L <= l && r <= R)
return sum[cur];
pushdown(cur, l, r);
int mid = l + r >> 1;
int res = 0;
if (L <= mid)
res = (res + query(ls(cur), l, mid, L, R)) % P;
if (mid + 1 <= R)
res = (res + query(rs(cur), mid + 1, r, L, R)) % P;
return res;
}
void path_add(int x, int y, int val) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]])
swap(x, y);
add(1, 1, N, dfn[top[x]], dfn[x], val);
x = fa[top[x]];
}
if (dep[x] < dep[y])
swap(x, y);
add(1, 1, N, dfn[y], dfn[x], val);
}
int path_query(int x, int y) {
int res = 0;
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]])
swap(x, y);
res = (res + query(1, 1, N, dfn[top[x]], dfn[x])) % P;
x = fa[top[x]];
}
if (dep[x] < dep[y])
swap(x, y);
return (res + query(1, 1, N, dfn[y], dfn[x])) % P;
}
signed main() {
cin >> N >> M >> R >> P;
for (int i = 1; i <= N; i++)
cin >> a[i];
for (int i = 1, x, y; i < N; i++) {
cin >> x >> y;
e[x].emplace_back(y);
e[y].emplace_back(x);
}
dfs1(R, 0, 1);
dfs2(R, R);
build(1, 1, N);
for (int opt, x, y, z; M--; ) {
cin >> opt;
if (opt == 1) {
cin >> x >> y >> z;
path_add(x, y, z);
} else if (opt == 2) {
cin >> x >> y;
cout << path_query(x, y) << '\n';
} else if (opt == 3) {
cin >> x >> z;
add(1, 1, N, dfn[x], dfn[x] + siz[x] - 1, z);
} else if (opt == 4) {
cin >> x;
cout << query(1, 1, N, dfn[x], dfn[x] + siz[x] - 1) << '\n';
}
}
return 0;
}
重链剖分的其它应用
重链剖分是一个特别实用的树上算法,或者说是数据结构,它的核心难点并不在于对树链的划分,而是如何选择正确的、最便捷的数据结构以减少本就庞大的代码量,例:
P8844 [传智杯 #4 初赛] 小卡与落叶(重链剖分+线段树/离线树状数组);
P2486 [SDOI2011] 染色(重链剖分+线段树);
P1505 [国家集训队] 旅游(重链剖分+线段树);
P2146 [NOI2015] 软件包管理器(重链剖分+线段树);
P7735 [NOI2021] 轻重边(重链剖分+线段树+抵消贡献);
P3313 [SDOI2014] 旅行(重链剖分+分块/线段树/平衡树);
P3401 洛谷树(重链剖分+线段树+树上前缀异或和+拆分贡献);

浙公网安备 33010602011771号