重链剖分

创建时间: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\) 次操作:

  1. \(x\)\(y\) 路径上的所有节点权值加上 \(z\)
  2. 查询 \(x\)\(y\) 路径上的点权之和对 \(P\) 取模;
  3. 将以 \(x\) 为根的子树中所有点的权值加上 \(z\)
  4. 查询以 \(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 洛谷树(重链剖分+线段树+树上前缀异或和+拆分贡献)

posted @ 2026-05-19 12:29  xubaichuan  阅读(7)  评论(0)    收藏  举报