古典叙事 · 技术札记

序章

山河有卷
人间有声

写代码,也写长风、旧城与未熄的灯。
愿每一篇随笔,都有自己的山水与回声。
阅览随笔

树链剖分问题

定义

树链剖分是一种对树进行划分的算法,将树分为多条链子,保证每个点只属于一条链,然后再通过数据结构(树状数组,BST,SPLAY,线段树等)来维护每一条链。

分类

一般分为两种,重链剖分和长链剖分

重链剖分:

定义

重儿子:在该节点的儿子中,子数数量最多的节点(如果存在多个最大的,选其一即可)(叶子节点无重儿子)

轻儿子:在该节点的儿子中,除重儿子之外的节点

重边:节点和其重儿子连成的边

轻边:节点和其轻儿子连成的边

重链:多条重边连成的路径

轻链:多条轻边连成的路径
把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。
如下图所示
image

实现

那么如何实现剖分成重链的过程呢
我们首先进行一次dfs,记录标记每个点的深度dep,[]标记每个点的父亲fa[]
标记每个非叶子节点的子树大小(含它自己),标记每个非叶子节点的重儿子编号son[]
代码如下:

void dfs(i64 now , i64 f , i64 deep)//分别表示现在在哪个节点,父亲是谁,深度为多少
{
	dep [ now ] = deep;//记录深度
	fa [ now ] = f;//记录父节点
	siz [ now ] = 1;//初始化子树大小
	i64 maxson = -1;
	for (auto it : t [ now ]) {
		if (it != f) {
			dfs(it , now , deep + 1);
			siz [ now ] += siz [ it ];//计算子树大小
			if (siz [ it ] > maxson) {
				son[now]=it;//找到重儿子
				maxson = siz [ it ];
			}
		}
	}
}

接下来我们还得进行一次dfs,用我们之前记录的信息去构造重链
完成
1.标记每个点的新编号
2.赋值每个点的初始值到新编号上
3.处理每个点所在链的顶端
4.处理每条链
代码如下

void dfss(i64 now,i64 ttop) //now表示现在的点,ttop表示链顶
{
	id [now ] = ++cnt;//更新点的编号
	top [ now ] = ttop;//更新top数组
	neww [ cnt ] = w [ now ];//将原点的值赋值给新点
	if (!son [ now ])return;//如果重儿子不存在,直接返回
	dfss(son [ now ] , ttop);
	for (auto it : t [ now ]) {
		if (it == fa [ now ] || it == son [ now ])
			continue;
		dfss(it , it);//所有的其他儿子都是链的top,继续递归
	}

}

现在我们应该用我们构造的重链去解决某些问题,以优化时间复杂度
模板题https://www.luogu.com.cn/problem/P3384

P3384 【模板】重链剖分/树链剖分

题目描述

如题,已知一棵包含 \(N\) 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

  • 1 x y z,表示将树从 x 到 y结点最短路径上所有节点的值都加上 z。

  • 2 x y,表示求树从 x 到 y 结点最短路径上所有节点的值之和。

  • 3 x z,表示将以 x 为根节点的子树内所有节点值都加上 z。

  • 4 x,表示求以 x 为根节点的子树内所有节点值之和。

题目要求我们完成这几个任务
总结一下我们刚刚构造链所得到的信息
1.因为顺序是先重再轻,所以每一条重链的新编号是连续的
2.因为是dfs,所以每一个子树的新编号也是连续的
image
当处理第一个和第二个问题时
我们要处理任意两点间路径
设所在链顶端的深度更深的那个点为x点
ans加上x点到x所在链顶端 这一段区间的点权和
把x跳到x所在链顶端的那个点的上面一个点,不停执行这两个步骤,直到两个点处于一条链上,这时再加上此时两个点的区间和即可
如下图:假如现在要处理4和9,那么9的top深度更深,然后往上跳一个点,所以我们计算[2,4]的区间和与[6,9]的区间和就是我们的答案
3591429-20250808190637997-1782872672

这时我们注意到,我们所要处理的所有区间均为连续编号(新编号),于是想到线段树,用线段树处理连续编号区间和
每次查询时间复杂度为O(log^2n)

i64 qrange(i64 x , i64 y) {
	i64 ans = 0;
	while (top [ x ] != top [ y ]) {
		if (dep [ x ] < dep [ y ])swap(x , y);//找到深度低的
		 res = 0;
		query(1 , 1 , n , id [ top [ x ] ] , id [ x ]);//计算区间和
		ans += res;
		ans %= mod;
		x = fa [ top [ x ] ];//更新x
	}
	if (dep [ x ] > dep [ y ])swap(x , y);//如果还没相等在计算最后一次
	res = 0;
	query(1 , 1 , n , id [ x ] , id [ y ]);
	ans += res;
	return ans%=mod;//返回查询结果
}

当处理第三个和第四个问题时、我们要处理一点及其子树的点权和:想到记录了每个非叶子节点的子树大小(含它自己),并且每个子树的新编号都是连续的,于是直接线段树区间查询即可时间复杂度为O(logn)

i64 qSon(i64 x) {
	return query(1, 1, n, id[x], id[x] + siz[x] - 1);//查找树上代表[id[x], id[x] + siz[x] - 1]的区间的节点值是多少
}

区间修改:

void updRange(i64 x, i64 y, i64 k) {
	k %= mod;
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y);
		update(1, 1, n, id[top[x]], id[x], k);//不断更新深度大的到他的top这一条路径上的区间
		x = fa[top[x]];
	}
	if (dep[x] > dep[y]) swap(x, y);
	update(1, 1, n, id[x], id[y], k);//最后更新一次
}

子树修改:

void updSon(i64 x, i64 k) {
	k %= mod;
	update(1, 1, n, id[x], id[x] + siz[x] - 1, k); //更新[id[x],id[x]+siz[x]-1]这一段区间
}

于是我们就可以通过这道题目

查看AC代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
using i64 = long long; // 使用长整型防止溢出

const i64 MAXN = 500005; // 最大节点数
i64 n, m, r, p; // n-节点数, m-操作数, r-根节点, p-模数
i64 w[MAXN]; // 原始节点权值
i64 neww[MAXN]; // 重编号后的节点权值
i64 id[MAXN]; // 节点的新编号(DFS序)
i64 top[MAXN]; // 节点所在重链的顶部节点
i64 son[MAXN]; // 节点的重儿子
i64 siz[MAXN]; // 以节点为根的子树大小
i64 fa[MAXN]; // 节点的父节点
i64 dep[MAXN]; // 节点的深度
vector<i64> t[MAXN]; // 树的邻接表表示

					 // 线段树相关变量
i64 a[MAXN * 4]; // 线段树节点值
i64 laz[MAXN * 4]; // 线段树懒标记
i64 mod; // 模数
i64 cnt = 0; // DFS序计数器

			 // 线段树: 下推懒标记
void pushdown(i64 rt, i64 len) {
	if (laz[rt] == 0) return;
	// 将懒标记下推到左右孩子
	laz[rt << 1] = (laz[rt << 1] + laz[rt]) % mod;
	laz[rt << 1 | 1] = (laz[rt << 1 | 1] + laz[rt]) % mod;
	// 更新左右孩子的值
	a[rt << 1] = (a[rt << 1] + laz[rt] * (len - (len >> 1))) % mod;
	a[rt << 1 | 1] = (a[rt << 1 | 1] + laz[rt] * (len >> 1)) % mod;
	laz[rt] = 0; // 清除当前节点的懒标记
}

// 线段树: 建树
void build(i64 rt, i64 l, i64 r) {
	if (l == r) {
		a[rt] = neww[l] % mod; // 叶子节点赋值
		return;
	}
	i64 mid = (l + r) >> 1;
	build(rt << 1, l, mid); // 递归构建左子树
	build(rt << 1 | 1, mid + 1, r); // 递归构建右子树
	a[rt] = (a[rt << 1] + a[rt << 1 | 1]) % mod; // 合并左右子树信息
}

// 线段树: 区间查询
i64 query(i64 rt, i64 l, i64 r, i64 L, i64 R) {
	if (L <= l && r <= R) {
		return a[rt]; // 当前区间完全包含在查询区间内
	}
	pushdown(rt, r - l + 1); // 下推懒标记
	i64 mid = (l + r) >> 1;
	i64 res = 0;
	if (L <= mid) res = (res + query(rt << 1, l, mid, L, R)) % mod; // 查询左子树
	if (R > mid) res = (res + query(rt << 1 | 1, mid + 1, r, L, R)) % mod; // 查询右子树
	return res;
}

// 线段树: 区间更新
void update(i64 rt, i64 l, i64 r, i64 L, i64 R, i64 k) {
	if (L <= l && r <= R) {
		// 当前区间完全包含在更新区间内
		laz[rt] = (laz[rt] + k) % mod; // 更新懒标记
		a[rt] = (a[rt] + k * (r - l + 1)) % mod; // 更新当前节点值
		return;
	}
	pushdown(rt, r - l + 1); // 下推懒标记
	i64 mid = (l + r) >> 1;
	if (L <= mid) update(rt << 1, l, mid, L, R, k); // 更新左子树
	if (R > mid) update(rt << 1 | 1, mid + 1, r, L, R, k); // 更新右子树
	a[rt] = (a[rt << 1] + a[rt << 1 | 1]) % mod; // 合并左右子树信息
}

// 第一次DFS: 计算子树大小、深度、父节点和重儿子
void dfs1(i64 now, i64 f, i64 deep) {
	dep[now] = deep; // 记录深度
	fa[now] = f; // 记录父节点
	siz[now] = 1; // 初始化子树大小为1(自己)
	i64 maxson = -1; // 用于寻找重儿子
	for (auto it : t[now]) {
		if (it != f) {
			dfs1(it, now, deep + 1); // 递归处理子节点
			siz[now] += siz[it]; // 累加子树大小
			if (siz[it] > maxson) {
				son[now] = it; // 更新重儿子
				maxson = siz[it];
			}
		}
	}
}

// 第二次DFS: 进行重链剖分,分配新编号
void dfs2(i64 now, i64 ttop) {
	id[now] = ++cnt; // 分配新编号
	top[now] = ttop; // 记录所在重链的顶部
	neww[cnt] = w[now]; // 记录新编号对应的权值
	if (!son[now]) return; // 没有重儿子(叶子节点)则返回
	dfs2(son[now], ttop); // 优先处理重儿子(保证重链编号连续)
	for (auto it : t[now]) {
		// 处理轻儿子(开启新的重链)
		if (it == fa[now] || it == son[now]) continue;
		dfs2(it, it); // 轻儿子的顶部是自己
	}
}

// 查询树上两点路径上的权值和
i64 qRange(i64 x, i64 y) {
	i64 ans = 0;
	// 当两点不在同一条重链上时
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y); // 让x所在重链顶部更深
												   // 查询当前重链的贡献
		ans = (ans + query(1, 1, n, id[top[x]], id[x])) % mod;
		x = fa[top[x]]; // 跳到上一条重链
	}
	// 两点已在同一条重链上
	if (dep[x] > dep[y]) swap(x, y); // 保证x是更浅的节点
	ans = (ans + query(1, 1, n, id[x], id[y])) % mod; // 查询最后一段
	return ans;
}

// 查询子树权值和
i64 qSon(i64 x) {
	// 子树在DFS序上是连续的[id[x], id[x]+siz[x]-1]
	return query(1, 1, n, id[x], id[x] + siz[x] - 1);
}

// 更新树上两点路径上的权值
void updRange(i64 x, i64 y, i64 k) {
	k %= mod;
	// 类似查询操作,分段更新
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]]) swap(x, y);
		update(1, 1, n, id[top[x]], id[x], k); // 更新当前重链
		x = fa[top[x]]; // 跳到上一条重链
	}
	if (dep[x] > dep[y]) swap(x, y);
	update(1, 1, n, id[x], id[y], k); // 更新最后一段
}

// 更新子树权值
void updSon(i64 x, i64 k) {
	k %= mod;
	// 子树在DFS序上是连续的[id[x], id[x]+siz[x]-1]
	update(1, 1, n, id[x], id[x] + siz[x] - 1, k);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);

	// 输入数据
	cin >> n >> m >> r >> p;
	mod = p;
	for (int i = 1; i <= n; ++i) {
		cin >> w[i]; // 节点权值
	}
	for (int i = 1; i < n; ++i) {
		i64 x, y;
		cin >> x >> y;
		t[x].push_back(y); // 构建树结构
		t[y].push_back(x);
	}

	// 树链剖分预处理
	dfs1(r, 0, 1); // 第一次DFS
	dfs2(r, r); // 第二次DFS
	build(1, 1, n); // 构建线段树

					// 处理操作
	while (m--) {
		i64 op, x, y, z;
		cin >> op;
		if (op == 1) {
			// 路径更新: x到y路径上所有节点加z
			cin >> x >> y >> z;
			updRange(x, y, z);
		} else if (op == 2) {
			// 路径查询: x到y路径上所有节点权值和
			cin >> x >> y;
			cout << qRange(x, y) << '\n';
		} else if (op == 3) {
			// 子树更新: x的子树中所有节点加z
			cin >> x >> z;
			updSon(x, z);
		} else if (op == 4) {
			// 子树查询: x的子树中所有节点权值和
			cin >> x;
			cout << qSon(x) << '\n';
		}
	}
	return 0;
}
posted @ 2025-08-12 16:16  Morphis‘  阅读(31)  评论(0)    收藏  举报

特别策划 · CINEMATIC NOTES

风沙与孤骑

风起塞外,
胜负在刀剑之前。

“把复杂拆成秩序,把未知写成答案。”
Morphis · 山河一卷 愿你从这里出发,仍能听见山风。 影像:farfarSébastien Goldberg / Unsplash