【笔记/模板】树链剖分
树链剖分
树链剖分的基本思想
通过将树分割成链的形式,从而把树形变为线性结构,减少处理难度。
树链剖分(树剖/链剖)有多种形式,如 重链剖分,长链剖分 和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」),大多数情况下(没有特别说明时),「树链剖分」都指「重链剖分」。
树链有如下几个特征:
- 一棵树上的任意一条链的长度不超过 \(\log_2 n\),且一条链上的各个节点深度互不相同(相对于根节点而言)。
- 通过特殊的遍历方式,树链剖分可以保证一条链上的 DFS 序连续,从而更加方便地使用线段树或树状数组维护树上的区间信息。
重链剖分
重链剖分的基本定义
重链剖分,顾名思义,是一种通过子节点大小进行剖分的形式,我们给出以下定义:
-
重子节点:一个非叶子节点中子树最大的子节点,如果存在多个则取其一。
-
轻子节点:除了重子节点以外的所有子节点。
-
重边:连接任意两个重儿子的边。
-
轻边:除了重边的其他所有树边。
-
重链:若干条重边首尾相连而成的一条链。
我们将落单的叶子节点本身看做重子节点,不难发现,整棵树就被剖分成了一条条重链。
需要注意的是,每一条重链都以轻子节点为起点。
实现
树链剖分的处理通过两次 DFS 遍历完成。
对于第一次 DFS,我们求出如下数值:
-
任意节点到达根的距离(即其深度):
depth[]
-
任意节点的父亲节点(根节点默认为 \(0\)):
fa[]
-
任意节点子树的大小(包括其本身):
sz[]
-
任意节点的重子节点(没有则为 \(0\)):
hson[]
void dfs1(int ver, int pre, int deep)
{
depth[ver] = deep, fa[ver] = pre, sz[ver] = 1;
int maxn = -1;
for (int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
if (j == pre) continue;
dfs1(j, ver, deep + 1);
sz[ver] += sz[j];
if (maxn == -1 || maxn < sz[j]) maxn = sz[j], hson[ver] = j;
// 更新重子节点
}
}
对于第二次 DFS,我们求出如下数值:
-
遍历时的各个节点的 dfs 序:
dfn[]
-
每个节点所属重链的最顶端节点:
top[]
-
dfs 序对应的节点编号:
id[]
,有 \(id(dfn(x)) = x\) -
每个节点在 dfs 序上的对应权值:
val[]
void dfs2(int ver, int topf)
{
dfn[ver] = ++ timestamp, val[timestamp] = a[ver], top[ver] = topf;
if (!hson[ver]) return;
dfs2(hson[ver], topf); // 先遍历重子节点
for (int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa[ver] || j == hson[ver]) continue;
dfs2(j, j); // 再遍历轻子节点
}
}
之所以要先遍历重子节点,是因为我们要保证重链上的 dfs 序连续,这样才可以进行区间操作,按照 \(dfn\) 排序后的序列即为剖分后的链。
重链剖分的性质
-
树上每个节点都属于且仅属于一条重链。
-
所有的重链将整棵树 完全剖分。
-
当我们向下经过一条 轻边 时,所在子树的大小至少会除以二,保证了复杂度的正确性。
常见应用
维护路径权值和
选取左右端点所在树中深度更大的节点,维护它到所在重链顶端的区间信息,之后不断上跳,知道它和另一端点在同一链上,维护两点之间的信息。使用线段树或者树状数组等数据结构,即可在 \(O(\log^2 n)\) 的时间内单次维护查询。
void modify_range(int x, int y, int k)
{
while (top[x] != top[y])
{
if (depth[top[x]] < depth[top[y]]) swap(x, y);
SGT.modify(1, dfn[top[x]], dfn[x], k);
x = fa[top[x]];
}
if (depth[x] > depth[y]) swap(x, y);
SGT.modify(1, dfn[x], dfn[y], k);
}
int query_range(int x, int y)
{
int res = 0;
while (top[x] != top[y])
{
if (depth[top[x]] < depth[top[y]]) swap(x, y);
res = (res + SGT.query(1, dfn[top[x]], dfn[x])) % mod;
x = fa[top[x]];
}
if (depth[x] > depth[y]) swap(x, y);
res = (res + SGT.query(1, dfn[x], dfn[y])) % mod;
return res;
}
维护子树信息
思路相似,但更加简单,经过 dfn 重新划分后,一颗子树的 dfn 序列一定在 \([dfn[x], dfn[x] +sz[x] - 1]\) 之间,单次维护即可,时间复杂度 \(O(\log n)\)。
void modify_subtree(int x, int k)
{
SGT.modify(1, dfn[x], dfn[x] + sz[x] - 1, k);
}
int query_subtree(int x)
{
return SGT.query(1, dfn[x], dfn[x] + sz[x] - 1);
}
求 LCA
与倍增求法相似,但常数更小。
每次选取重链顶端节点深度更大的节点上跳,知道两者在同一重链上,此时深度较小者为两节点 LCA。
int lca(int a, int b)
{
while (top[a] != top[b])
{
if (depth[top[a]] > depth[top[b]]) a = fa[top[a]];
else b = fa[top[b]];
}
return depth[a] < depth[b] ? a : b;
}
例题 & Code
// Problem: P3384 【模板】重链剖分/树链剖分
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3384
// Memory Limit: 128 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
// #define int long long
#define DEBUG
#define lc u << 1
#define rc u << 1 | 1
#define File(a) freopen(a".in", "r", stdin); freopen(a".out", "w", stdout)
typedef long long LL;
typedef pair<int, int> PII;
const int N = 100010, M = N << 1;
const int INF = 0x3f3f3f3f;
int n, m, root, mod;
int h[N], e[M], ne[M], idx;
int a[N], val[N];
int depth[N], fa[N], sz[N], hson[N];
int dfn[N], timestamp;
int top[N], id[N];
struct Tree
{
struct Node
{
int l, r, sum, tag;
inline int len() {return r - l + 1; }
} tr[N << 2];
void pushup(int u)
{
tr[u].sum = (tr[lc].sum + tr[rc].sum) % mod;
}
void build(int u, int l, int r)
{
tr[u].l = l, tr[u].r = r;
if (l == r) return tr[u].sum = val[l], void(0);
int mid = l + r >> 1;
build(lc, l, mid), build(rc, mid + 1, r);
pushup(u);
}
void pushdown(int u)
{
if (!tr[u].tag) return;
tr[lc].sum = (tr[lc].sum + tr[u].tag * tr[lc].len()) % mod;
tr[rc].sum = (tr[rc].sum + tr[u].tag * tr[rc].len()) % mod;
tr[lc].tag += tr[u].tag, tr[rc].tag += tr[u].tag;
tr[u].tag = 0;
}
void modify(int u, int l, int r, int k)
{
if (l <= tr[u].l && tr[u].r <= r)
{
tr[u].sum = (tr[u].sum + tr[u].len() * k) % mod;
tr[u].tag += k;
return;
}
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(lc, l, r, k);
if (r > mid) modify(rc, l, r, k);
pushup(u);
}
int 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;
int res = 0;
if (l <= mid) res = (res + query(lc, l, r)) % mod;
if (r > mid) res = (res + query(rc, l, r)) % mod;
return res;
}
} SGT;
inline void add(int a, int b)
{
e[++ idx] = b, ne[idx] = h[a], h[a] = idx;
}
void dfs1(int ver, int pre, int deep)
{
depth[ver] = deep, fa[ver] = pre, sz[ver] = 1;
int maxn = -1;
for (int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
if (j == pre) continue;
dfs1(j, ver, deep + 1);
sz[ver] += sz[j];
if (maxn == -1 || maxn < sz[j]) maxn = sz[j], hson[ver] = j;
}
}
void dfs2(int ver, int topf)
{
dfn[ver] = ++ timestamp, val[timestamp] = a[ver], top[ver] = topf;
if (!hson[ver]) return;
dfs2(hson[ver], topf);
for (int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa[ver] || j == hson[ver]) continue;
dfs2(j, j);
}
}
void modify_range(int x, int y, int k)
{
while (top[x] != top[y])
{
if (depth[top[x]] < depth[top[y]]) swap(x, y);
SGT.modify(1, dfn[top[x]], dfn[x], k);
x = fa[top[x]];
}
if (depth[x] > depth[y]) swap(x, y);
SGT.modify(1, dfn[x], dfn[y], k);
}
int query_range(int x, int y)
{
int res = 0;
while (top[x] != top[y])
{
if (depth[top[x]] < depth[top[y]]) swap(x, y);
res = (res + SGT.query(1, dfn[top[x]], dfn[x])) % mod;
x = fa[top[x]];
}
if (depth[x] > depth[y]) swap(x, y);
res = (res + SGT.query(1, dfn[x], dfn[y])) % mod;
return res;
}
void modify_subtree(int x, int k)
{
SGT.modify(1, dfn[x], dfn[x] + sz[x] - 1, k);
}
int query_subtree(int x)
{
return SGT.query(1, dfn[x], dfn[x] + sz[x] - 1);
}
signed main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m >> root >> mod;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i < n; i ++)
{
int u, v; cin >> u >> v;
add(u, v), add(v, u);
}
dfs1(root, 0, 1);
dfs2(root, root);
SGT.build(1, 1, n);
while (m --)
{
int opt; cin >> opt;
if (opt == 1)
{
int x, y, z; cin >> x >> y >> z;
modify_range(x, y, z);
}
else if (opt == 2)
{
int x, y; cin >> x >> y;
cout << query_range(x, y) % mod << '\n';
}
else if (opt == 3)
{
int x, y; cin >> x >> y;
modify_subtree(x, y);
}
else
{
int x; cin >> x;
cout << query_subtree(x) % mod << '\n';
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!