我来详细讲解 树链剖分(Heavy-Light Decomposition, HLD),这是一种将树结构转化为线性序列的强大技术,使得树上的路径问题可以转化为区间问题,从而用线段树等数据结构高效解决。
核心思想
树链剖分将一棵树分解为若干条重链 (Heavy Path),使得从任意节点到根的路径最多经过 O(log n) 条重链。
关键概念:
-
重儿子:子树大小最大的儿子
-
轻儿子:其他儿子
-
重边:连接节点与其重儿子的边
-
重链:由重边组成的链
-
轻边:连接节点与其轻儿子的边
树结构:
1
/
2 3
/| |
4 5 6
/ /
7 8 9重链剖分(假设子树大小:1>2>3,2>4>5,4>7,6>8>9):
重链1:1 - 2 - 4 - 7(主链)
重链2:3 - 6 - 8
重链3:5
重链4:9性质:任意路径 u→v 可以拆分为 O(log n) 段连续的 DFS 序区间
完整实现:路径修改 + 路径查询
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, m, root;
int mod;
// 原图
vector<int> g[N];
int w[N]; // 节点权值
// 树链剖分数组
int fa[N]; // 父节点
int dep[N]; // 深度
int siz[N]; // 子树大小
int son[N]; // 重儿子(0表示无)
int top[N]; // 所在重链的顶端节点
int dfn[N]; // DFS序(时间戳)
int rnk[N]; // dfn的逆:rnk[dfn[u]] = u
int wval[N]; // 按DFS序排列的权值(用于线段树)
int tim; // 时间戳计数器
// ========== 第一步:DFS1 求 fa, dep, siz, son ==========
void dfs1(int u, int f) {
fa[u] = f;
dep[u] = dep[f] + 1;
siz[u] = 1;
son[u] = 0;
for (int v : g[u]) {
if (v == f) continue;
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v; // 更新重儿子
}
}
// ========== 第二步:DFS2 求 top, dfn, 剖分重链 ==========
void dfs2(int u, int t) {
top[u] = t;
dfn[u] = ++tim;
rnk[tim] = u;
wval[tim] = w[u]; // 按DFS序存储权值
if (!son[u]) return; // 叶子节点
// 重儿子先走,保证重链上DFS序连续
dfs2(son[u], t);
// 轻儿子各自成链
for (int v : g[u]) {
if (v == fa[u] || v == son[u]) continue;
dfs2(v, v); // 轻儿子的top是自己
}
}
// ========== 线段树部分(区间修改、区间查询) ==========
struct Node {
int l, r;
long long sum, add;
} tr[N * 4];
void pushup(int u) {
tr[u].sum = (tr[u<<1].sum + tr[u<<1|1].sum) % mod;
}
void apply(int u, long long v) {
tr[u].sum = (tr[u].sum + (tr[u].r - tr[u].l + 1) * v) % mod;
tr[u].add = (tr[u].add + v) % mod;
}
void pushdown(int u) {
if (tr[u].add) {
apply(u<<1, tr[u].add);
apply(u<<1|1, tr[u].add);
tr[u].add = 0;
}
}
void build(int u, int l, int r) {
tr[u] = {l, r, wval[l], 0};
if (l == r) return;
int mid = l + r >> 1;
build(u<<1, l, mid);
build(u<<1|1, mid+1, r);
pushup(u);
}
void modify(int u, int l, int r, long long v) {
if (l <= tr[u].l && tr[u].r <= r) {
apply(u, v);
return;
}
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u<<1, l, r, v);
if (r > mid) modify(u<<1|1, l, r, v);
pushup(u);
}
long long query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
long long res = 0;
if (l <= mid) res = (res + query(u<<1, l, r)) % mod;
if (r > mid) res = (res + query(u<<1|1, l, r)) % mod;
return res;
}
// ========== 树链剖分核心操作:路径修改与查询 ==========
// 将路径 u→v 上的所有节点加 z
void pathModify(int u, int v, long long z) {
z %= mod;
while (top[u] != top[v]) { // 不在同一条重链
if (dep[top[u]] < dep[top[v]]) swap(u, v); // 保证u所在链更深
modify(1, dfn[top[u]], dfn[u], z); // 修改整条重链
u = fa[top[u]]; // 跳到链头的父节点
}
// 现在在同一条重链上
if (dep[u] > dep[v]) swap(u, v); // 保证u是LCA
modify(1, dfn[u], dfn[v], z); // 修改最后一段
}
// 查询路径 u→v 上的权值和
long long pathQuery(int u, int v) {
long long res = 0;
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
res = (res + query(1, dfn[top[u]], dfn[u])) % mod;
u = fa[top[u]];
}
if (dep[u] > dep[v]) swap(u, v);
res = (res + query(1, dfn[u], dfn[v])) % mod;
return res;
}
// ========== 子树操作(利用DFS序性质) ==========
// 子树所有节点的DFS序是连续的 [dfn[u], dfn[u] + siz[u] - 1]
void subtreeModify(int u, long long z) {
modify(1, dfn[u], dfn[u] + siz[u] - 1, z % mod);
}
long long subtreeQuery(int u) {
return query(1, dfn[u], dfn[u] + siz[u] - 1);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> root >> mod;
for (int i = 1; i <= n; i++) cin >> w[i];
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
// 树链剖分
dfs1(root, 0);
dfs2(root, root);
// 建线段树
build(1, 1, n);
while (m--) {
int op, x, y;
long long z;
cin >> op;
if (op == 1) { // 路径修改
cin >> x >> y >> z;
pathModify(x, y, z);
} else if (op == 2) { // 路径查询
cin >> x >> y;
cout << pathQuery(x, y) << '\n';
} else if (op == 3) { // 子树修改
cin >> x >> z;
subtreeModify(x, z);
} else { // 子树查询
cin >> x;
cout << subtreeQuery(x) << '\n';
}
}
return 0;
}
关键过程图解
路径查询/修改的拆分过程
查询路径 7 → 9:
7 在重链 [1-2-4-7] 上,top[7]=1
9 在重链 [3-6-9] 上,top[9]=3
第一次循环:
top[7]=1, top[9]=3, 不同
dep[1]=1 < dep[3]=2, 交换:u=9, v=7
查询 [dfn[3], dfn[9]] = [5,7](重链3-6-8-9?不对,看具体剖分)
u = fa[3] = 1
第二次循环:
top[1]=1, top[7]=1, 相同!
查询 [dfn[1], dfn[7]] = [1,4]
总共两段,O(log n) 次线段树操作
进阶:LCA(最近公共祖先)
树链剖分可以 O(log n) 求 LCA:
cpp
int lca(int u, int v) {
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
u = fa[top[u]];
}
return dep[u] < dep[v] ? u : v;
}
进阶:维护边权
将边权转化为点权:每条边的权值赋给子节点。
cpp
// 边 (u,v,w),假设 v 是 u 的儿子
// 把 w 存在 v 上
w[v] = 边权;
// 查询 u→v 路径上的最大边权(u是v的祖先)
int queryEdge(int u, int v) { // u是v的祖先
int res = 0;
while (top[u] != top[v]) {
res = max(res, seg.queryMax(dfn[top[v]], dfn[v]));
v = fa[top[v]];
}
// 现在同一条链,u是祖先
if (u != v) // 排除u本身(边权在子节点上)
res = max(res, seg.queryMax(dfn[u]+1, dfn[v]));
return res;
}
进阶:换根操作
维护以不同节点为根时的子树信息。
cpp
// 判断 u 是否是 v 的祖先
bool isAncestor(int u, int v) {
return dfn[u] <= dfn[v] && dfn[v] < dfn[u] + siz[u];
}
// 换根后,x的子树是什么?
// 设当前根为root,求x的子树
int getRootedSubtree(int root, int x) {
if (root == x) return 1; // 整棵树
if (!isAncestor(x, root)) {
// x的子树不变
return seg.query(dfn[x], dfn[x] + siz[x] - 1);
}
// root在x的某个儿子的子树中,x的子树是整棵树减去那个儿子的子树
int y = jump(root, dep[root] - dep[x] - 1); // 从root往上跳到x的儿子
return total - seg.query(dfn[y], dfn[y] + siz[y] - 1);
}
// 从u向上跳k步
int jump(int u, int k) {
while (k) {
if (dfn[u] - dfn[top[u]] + 1 <= k) {
k -= dfn[u] - dfn[top[u]] + 1;
u = fa[top[u]];
} else {
return rnk[dfn[u] - k];
}
}
return u;
}
完整封装模板
cpp
template<typename T, typename SegTree>
struct TreeChain {
int n, root;
vector<vector<int>> g;
vector<int> fa, dep, siz, son, top, dfn, rnk;
vector<T> w, wval;
SegTree seg;
int tim;
TreeChain(int n = 0, int root = 1) { init(n, root); }
void init(int n_, int root_) {
n = n_; root = root_;
g.assign(n + 1, {});
fa = dep = siz = son = top = dfn = rnk = vector<int>(n + 1);
w = wval = vector<T>(n + 1);
tim = 0;
}
void addEdge(int u, int v) {
g[u].push_back(v);
g[v].push_back(u);
}
void setWeight(int u, T val) { w[u] = val; }
void build() {
dfs1(root, 0);
dfs2(root, root);
seg.build(wval, 1, n);
}
void dfs1(int u, int f) {
fa[u] = f; dep[u] = dep[f] + 1; siz[u] = 1; son[u] = 0;
for (int v : g[u]) if (v != f) {
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v;
}
}
void dfs2(int u, int t) {
top[u] = t; dfn[u] = ++tim; rnk[tim] = u; wval[tim] = w[u];
if (!son[u]) return;
dfs2(son[u], t);
for (int v : g[u]) if (v != fa[u] && v != son[u])
dfs2(v, v);
}
// 路径操作
void pathModify(int u, int v, T val) {
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
seg.modify(dfn[top[u]], dfn[u], val);
u = fa[top[u]];
}
if (dep[u] > dep[v]) swap(u, v);
seg.modify(dfn[u], dfn[v], val);
}
T pathQuery(int u, int v) {
T res = 0;
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
res = res + seg.query(dfn[top[u]], dfn[u]);
u = fa[top[u]];
}
if (dep[u] > dep[v]) swap(u, v);
return res + seg.query(dfn[u], dfn[v]);
}
// 子树操作
void subtreeModify(int u, T val) {
seg.modify(dfn[u], dfn[u] + siz[u] - 1, val);
}
T subtreeQuery(int u) {
return seg.query(dfn[u], dfn[u] + siz[u] - 1);
}
int lca(int u, int v) {
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
u = fa[top[u]];
}
return dep[u] < dep[v] ? u : v;
}
};
时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 树链剖分(两次DFS) | O(n) | 预处理 |
| 路径修改/查询 | O(log² n) | 最多 O(log n) 条重链,每条线段树 O(log n) |
| 子树修改/查询 | O(log n) | DFS序连续,直接线段树 |
| LCA | O(log n) | 跳重链 |
| 换根子树查询 | O(log n) | 配合额外判断 |
注意:如果线段树换成 O(1) 查询的数据结构(如ST表),路径查询可优化到 O(log n)。
应用场景总结
| 应用场景 | 关键技巧 |
|---|---|
| 树上路径修改/查询 | 拆分为重链区间 |
| 子树修改/查询 | 利用DFS序连续性 |
| 边权维护 | 下放到子节点 |
| 换根问题 | 分类讨论root位置 |
| 树上线段树合并 | 配合DSU on tree |
| 动态树问题 | 结合Link-Cut Tree |
与其他算法的对比
| 算法 | 功能 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| 树链剖分 | 路径/子树操作 | O(log² n) | 静态树,频繁路径查询 |
| LCT (Link-Cut Tree) | 动态树,换根、连边、断边 | 均摊 O(log n) | 动态树结构 |
| 欧拉序+线段树 | 子树操作 | O(log n) | 只有子树操作,无路径 |
| 倍增法 | LCA、祖先查询 | O(log n) 预处理,O(1) 查询 | 只需LCA,无需修改 |
树链剖分是静态树上路径问题的标准解法 ,代码规整、效率优秀,是竞赛中的必备技能! 我来详细讲解 树链剖分(Heavy-Light Decomposition, HLD),这是一种将树结构转化为线性序列的强大技术,使得树上的路径问题可以转化为区间问题,从而用线段树等数据结构高效解决。
核心思想
树链剖分将一棵树分解为若干条重链 (Heavy Path),使得从任意节点到根的路径最多经过 O(log n) 条重链。
关键概念:
-
重儿子:子树大小最大的儿子
-
轻儿子:其他儿子
-
重边:连接节点与其重儿子的边
-
重链:由重边组成的链
-
轻边:连接节点与其轻儿子的边
树结构:
1
/
2 3
/| |
4 5 6
/ /
7 8 9重链剖分(假设子树大小:1>2>3,2>4>5,4>7,6>8>9):
重链1:1 - 2 - 4 - 7(主链)
重链2:3 - 6 - 8
重链3:5
重链4:9性质:任意路径 u→v 可以拆分为 O(log n) 段连续的 DFS 序区间
完整实现:路径修改 + 路径查询
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, m, root;
int mod;
// 原图
vector<int> g[N];
int w[N]; // 节点权值
// 树链剖分数组
int fa[N]; // 父节点
int dep[N]; // 深度
int siz[N]; // 子树大小
int son[N]; // 重儿子(0表示无)
int top[N]; // 所在重链的顶端节点
int dfn[N]; // DFS序(时间戳)
int rnk[N]; // dfn的逆:rnk[dfn[u]] = u
int wval[N]; // 按DFS序排列的权值(用于线段树)
int tim; // 时间戳计数器
// ========== 第一步:DFS1 求 fa, dep, siz, son ==========
void dfs1(int u, int f) {
fa[u] = f;
dep[u] = dep[f] + 1;
siz[u] = 1;
son[u] = 0;
for (int v : g[u]) {
if (v == f) continue;
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v; // 更新重儿子
}
}
// ========== 第二步:DFS2 求 top, dfn, 剖分重链 ==========
void dfs2(int u, int t) {
top[u] = t;
dfn[u] = ++tim;
rnk[tim] = u;
wval[tim] = w[u]; // 按DFS序存储权值
if (!son[u]) return; // 叶子节点
// 重儿子先走,保证重链上DFS序连续
dfs2(son[u], t);
// 轻儿子各自成链
for (int v : g[u]) {
if (v == fa[u] || v == son[u]) continue;
dfs2(v, v); // 轻儿子的top是自己
}
}
// ========== 线段树部分(区间修改、区间查询) ==========
struct Node {
int l, r;
long long sum, add;
} tr[N * 4];
void pushup(int u) {
tr[u].sum = (tr[u<<1].sum + tr[u<<1|1].sum) % mod;
}
void apply(int u, long long v) {
tr[u].sum = (tr[u].sum + (tr[u].r - tr[u].l + 1) * v) % mod;
tr[u].add = (tr[u].add + v) % mod;
}
void pushdown(int u) {
if (tr[u].add) {
apply(u<<1, tr[u].add);
apply(u<<1|1, tr[u].add);
tr[u].add = 0;
}
}
void build(int u, int l, int r) {
tr[u] = {l, r, wval[l], 0};
if (l == r) return;
int mid = l + r >> 1;
build(u<<1, l, mid);
build(u<<1|1, mid+1, r);
pushup(u);
}
void modify(int u, int l, int r, long long v) {
if (l <= tr[u].l && tr[u].r <= r) {
apply(u, v);
return;
}
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u<<1, l, r, v);
if (r > mid) modify(u<<1|1, l, r, v);
pushup(u);
}
long long query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
long long res = 0;
if (l <= mid) res = (res + query(u<<1, l, r)) % mod;
if (r > mid) res = (res + query(u<<1|1, l, r)) % mod;
return res;
}
// ========== 树链剖分核心操作:路径修改与查询 ==========
// 将路径 u→v 上的所有节点加 z
void pathModify(int u, int v, long long z) {
z %= mod;
while (top[u] != top[v]) { // 不在同一条重链
if (dep[top[u]] < dep[top[v]]) swap(u, v); // 保证u所在链更深
modify(1, dfn[top[u]], dfn[u], z); // 修改整条重链
u = fa[top[u]]; // 跳到链头的父节点
}
// 现在在同一条重链上
if (dep[u] > dep[v]) swap(u, v); // 保证u是LCA
modify(1, dfn[u], dfn[v], z); // 修改最后一段
}
// 查询路径 u→v 上的权值和
long long pathQuery(int u, int v) {
long long res = 0;
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
res = (res + query(1, dfn[top[u]], dfn[u])) % mod;
u = fa[top[u]];
}
if (dep[u] > dep[v]) swap(u, v);
res = (res + query(1, dfn[u], dfn[v])) % mod;
return res;
}
// ========== 子树操作(利用DFS序性质) ==========
// 子树所有节点的DFS序是连续的 [dfn[u], dfn[u] + siz[u] - 1]
void subtreeModify(int u, long long z) {
modify(1, dfn[u], dfn[u] + siz[u] - 1, z % mod);
}
long long subtreeQuery(int u) {
return query(1, dfn[u], dfn[u] + siz[u] - 1);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> root >> mod;
for (int i = 1; i <= n; i++) cin >> w[i];
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
// 树链剖分
dfs1(root, 0);
dfs2(root, root);
// 建线段树
build(1, 1, n);
while (m--) {
int op, x, y;
long long z;
cin >> op;
if (op == 1) { // 路径修改
cin >> x >> y >> z;
pathModify(x, y, z);
} else if (op == 2) { // 路径查询
cin >> x >> y;
cout << pathQuery(x, y) << '\n';
} else if (op == 3) { // 子树修改
cin >> x >> z;
subtreeModify(x, z);
} else { // 子树查询
cin >> x;
cout << subtreeQuery(x) << '\n';
}
}
return 0;
}
关键过程图解
路径查询/修改的拆分过程
查询路径 7 → 9:
7 在重链 [1-2-4-7] 上,top[7]=1
9 在重链 [3-6-9] 上,top[9]=3
第一次循环:
top[7]=1, top[9]=3, 不同
dep[1]=1 < dep[3]=2, 交换:u=9, v=7
查询 [dfn[3], dfn[9]] = [5,7](重链3-6-8-9?不对,看具体剖分)
u = fa[3] = 1
第二次循环:
top[1]=1, top[7]=1, 相同!
查询 [dfn[1], dfn[7]] = [1,4]
总共两段,O(log n) 次线段树操作
进阶:LCA(最近公共祖先)
树链剖分可以 O(log n) 求 LCA:
cpp
int lca(int u, int v) {
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
u = fa[top[u]];
}
return dep[u] < dep[v] ? u : v;
}
进阶:维护边权
将边权转化为点权:每条边的权值赋给子节点。
cpp
// 边 (u,v,w),假设 v 是 u 的儿子
// 把 w 存在 v 上
w[v] = 边权;
// 查询 u→v 路径上的最大边权(u是v的祖先)
int queryEdge(int u, int v) { // u是v的祖先
int res = 0;
while (top[u] != top[v]) {
res = max(res, seg.queryMax(dfn[top[v]], dfn[v]));
v = fa[top[v]];
}
// 现在同一条链,u是祖先
if (u != v) // 排除u本身(边权在子节点上)
res = max(res, seg.queryMax(dfn[u]+1, dfn[v]));
return res;
}
进阶:换根操作
维护以不同节点为根时的子树信息。
cpp
// 判断 u 是否是 v 的祖先
bool isAncestor(int u, int v) {
return dfn[u] <= dfn[v] && dfn[v] < dfn[u] + siz[u];
}
// 换根后,x的子树是什么?
// 设当前根为root,求x的子树
int getRootedSubtree(int root, int x) {
if (root == x) return 1; // 整棵树
if (!isAncestor(x, root)) {
// x的子树不变
return seg.query(dfn[x], dfn[x] + siz[x] - 1);
}
// root在x的某个儿子的子树中,x的子树是整棵树减去那个儿子的子树
int y = jump(root, dep[root] - dep[x] - 1); // 从root往上跳到x的儿子
return total - seg.query(dfn[y], dfn[y] + siz[y] - 1);
}
// 从u向上跳k步
int jump(int u, int k) {
while (k) {
if (dfn[u] - dfn[top[u]] + 1 <= k) {
k -= dfn[u] - dfn[top[u]] + 1;
u = fa[top[u]];
} else {
return rnk[dfn[u] - k];
}
}
return u;
}
完整封装模板
cpp
template<typename T, typename SegTree>
struct TreeChain {
int n, root;
vector<vector<int>> g;
vector<int> fa, dep, siz, son, top, dfn, rnk;
vector<T> w, wval;
SegTree seg;
int tim;
TreeChain(int n = 0, int root = 1) { init(n, root); }
void init(int n_, int root_) {
n = n_; root = root_;
g.assign(n + 1, {});
fa = dep = siz = son = top = dfn = rnk = vector<int>(n + 1);
w = wval = vector<T>(n + 1);
tim = 0;
}
void addEdge(int u, int v) {
g[u].push_back(v);
g[v].push_back(u);
}
void setWeight(int u, T val) { w[u] = val; }
void build() {
dfs1(root, 0);
dfs2(root, root);
seg.build(wval, 1, n);
}
void dfs1(int u, int f) {
fa[u] = f; dep[u] = dep[f] + 1; siz[u] = 1; son[u] = 0;
for (int v : g[u]) if (v != f) {
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v;
}
}
void dfs2(int u, int t) {
top[u] = t; dfn[u] = ++tim; rnk[tim] = u; wval[tim] = w[u];
if (!son[u]) return;
dfs2(son[u], t);
for (int v : g[u]) if (v != fa[u] && v != son[u])
dfs2(v, v);
}
// 路径操作
void pathModify(int u, int v, T val) {
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
seg.modify(dfn[top[u]], dfn[u], val);
u = fa[top[u]];
}
if (dep[u] > dep[v]) swap(u, v);
seg.modify(dfn[u], dfn[v], val);
}
T pathQuery(int u, int v) {
T res = 0;
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
res = res + seg.query(dfn[top[u]], dfn[u]);
u = fa[top[u]];
}
if (dep[u] > dep[v]) swap(u, v);
return res + seg.query(dfn[u], dfn[v]);
}
// 子树操作
void subtreeModify(int u, T val) {
seg.modify(dfn[u], dfn[u] + siz[u] - 1, val);
}
T subtreeQuery(int u) {
return seg.query(dfn[u], dfn[u] + siz[u] - 1);
}
int lca(int u, int v) {
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
u = fa[top[u]];
}
return dep[u] < dep[v] ? u : v;
}
};
时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 树链剖分(两次DFS) | O(n) | 预处理 |
| 路径修改/查询 | O(log² n) | 最多 O(log n) 条重链,每条线段树 O(log n) |
| 子树修改/查询 | O(log n) | DFS序连续,直接线段树 |
| LCA | O(log n) | 跳重链 |
| 换根子树查询 | O(log n) | 配合额外判断 |
注意:如果线段树换成 O(1) 查询的数据结构(如ST表),路径查询可优化到 O(log n)。
应用场景总结
| 应用场景 | 关键技巧 |
|---|---|
| 树上路径修改/查询 | 拆分为重链区间 |
| 子树修改/查询 | 利用DFS序连续性 |
| 边权维护 | 下放到子节点 |
| 换根问题 | 分类讨论root位置 |
| 树上线段树合并 | 配合DSU on tree |
| 动态树问题 | 结合Link-Cut Tree |
与其他算法的对比
| 算法 | 功能 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| 树链剖分 | 路径/子树操作 | O(log² n) | 静态树,频繁路径查询 |
| LCT (Link-Cut Tree) | 动态树,换根、连边、断边 | 均摊 O(log n) | 动态树结构 |
| 欧拉序+线段树 | 子树操作 | O(log n) | 只有子树操作,无路径 |
| 倍增法 | LCA、祖先查询 | O(log n) 预处理,O(1) 查询 | 只需LCA,无需修改 |
树链剖分是静态树上路径问题的标准解法,代码规整、效率优秀,是竞赛中的必备技能!