【学习笔记】树链剖分(重链剖分)

本文详细介绍了树链剖分的概念及其在数据结构中的应用,包括修改和查询树上路径节点权值、以节点为根的子树权值。通过DFS1和DFS2进行预处理,实现O(log²n)的修改和查询操作。同时提供了完整的无注释代码,涉及线段树等数据结构。
摘要由CSDN通过智能技术生成

重链剖分

概念

树链剖分可将树划分成若干条不相交的链的形式,每条链都是自底向上的。
重链剖分能保证划分出的每条重链与轻链上的节点 DFS 序连续,并以此作为节点的序号,每颗子树中的所有节点序号在 [ a , a + s i z e ] [a,a+size] [a,a+size]范围内(子树根节点序号为 a a a,子树大小为 s i z e size size)。
因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。

下列列举几个应用:

  • 修改、查询树上两节点之间的路径上所有节点的权值。
  • 修改、查询以某一节点为根的子树上所有节点的权值。
  • 查询两节点的最近公共祖先(lca)。
名词解释
重儿子对于每个非叶子节点,子树大小最大的儿子
轻儿子对于每个非叶子节点,除重儿子以外的儿子
重边从该节点到重儿子的边
轻边重边以外的边
重链由若干条首尾衔接的重边构成
轻链由若干条首尾衔接的轻边构成

特别地:对于只有一个儿子的节点,该儿子即为重儿子。

模板题链接(洛谷 P3384 【模板】轻重链剖分)

预处理 O ( 2 n ) O(2n) O(2n)

①DFS1()

处理每个节点的父亲、重儿子、子树大小、深度

变量解释
fa[ ]父亲
son[ ]重儿子
sz[ ]子树大小
dep[ ]深度
void dfs1(int fath, int u)
{
	fa[u] = fath;
	dep[u] = dep[fath] + 1;
	sz[u] = 1;
	for (int i = h[u]; i; i = ne[i])
	{
		int v = e[i];
		if (v == fath)continue;
		dfs1(u, v);
		sz[u] += sz[v];
		if (sz[v] > sz[son[u]])son[u] = v;
	}
}

数字为DFS序,红色节点为重儿子
DFS1

②DFS2()

根据重儿子DFS重链,使其DFS序连续,并标记为新编号,初始化权值

变量解释
top[ ]当前节点所在链的最顶端节点
id[ ]节点的新编号(即为DFS序)
w[ ]新编号对应的点权值
void dfs2(int u, int ptop)
{
	id[u] = ++cnt;
	w[cnt] = wt[u];
	top[u] = ptop;
	if (!son[u])return;
	dfs2(son[u], ptop);//先dfs重链,保证重链编号连续
	for (int i = h[u]; i; i = ne[i])
	{
		int v = e[i];
		if (v != fa[u] && v != son[u])
			dfs2(v, v);//轻链的顶端节点就是自己
	}
}

数字为DFS序(即新编号),紫色边为重链,红色节点为重儿子,不难发现子树的编号也是连续的
DFS2

修改查询操作

①对于任意两节点间的路径上所有节点

修改 O ( l o g 2 n ) O(log^{2}n) O(log2n)

如果两个节点处于两条不同的重链,则应让深度更深的节点跳到该链顶端的父亲节点(类似LCA),并在此期间要使用数据结构维护路径上的权值(同一条链上的编号连续)。

void modify(int a, int b, int z)//给a,b两节点间的路径权值加上z
{
	while (top[a] != top[b])
	{
		if (dep[top[a]] < dep[top[b]])swap(a, b);//注意要让该链顶端节点更深的的节点先跳
		//(易错)不是 dep[a] < dep[b]
		update(1, 1, n, id[top[a]], id[a], z);
		a = fa[top[a]];
	}
	if (dep[a] > dep[b])swap(a, b);
	update(1, 1, n, id[a], id[b], z);
}
查询 O ( l o g 2 n ) O(log^{2}n) O(log2n)

思路与修改类似

ll ask(int a, int b)
{
	ll ans = 0;
	while (top[a] != top[b])
	{
		if (dep[top[a]] < dep[top[b]])swap(a, b);
		ans += query(1, 1, n, id[top[a]], id[a]);
		a = fa[top[a]];
	}
	if (dep[a] > dep[b])swap(a, b);
	ans += query(1, 1, n, id[a], id[b]);
	return ans;
}

②对于以任意节点为根的子树

由于子树的编号是连续的,前面的预处理中已标记每个节点的子树大小,因此可以直接使用数据结构维护

修改 O ( l o g n ) O(logn) O(logn)
void modify(int x, int z)//给以z为根的子树加上z
{
	update(1, 1, n, id[x], id[x] + sz[x] - 1, z);
}
查询 O ( l o g n ) O(logn) O(logn)
ll ask(int x)
{
	return query(1, 1, n, id[x], id[x] + sz[x] - 1);
}

数据结构

数据结构可以灵活选择,这里不作赘述,完整代码中的数据结构为线段树

完整代码(无注释)

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

typedef long long ll;
const int N = 2e5 + 10;
ll wt[N], w[N], P;
int n, m, r;

#define lp p<<1
#define rp p<<1|1
#define lson lp,l,mid
#define rson rp,mid+1,r
struct { ll sum, tag, len; }t[N << 2];

void build(int p, int l, int r)
{
	t[p].len = r - l + 1;
	if (l == r)
	{
		t[p].sum = w[l] % P;
		return;
	}
	int mid = l + r >> 1;
	build(lson), build(rson);
	t[p].sum = (t[lp].sum + t[rp].sum) % P;
}

void pushdown(int p)
{
	if (!t[p].tag)return;
	t[lp].sum = (t[lp].sum + t[p].tag * t[lp].len) % P;
	t[rp].sum = (t[rp].sum + t[p].tag * t[rp].len) % P;
	t[lp].tag = (t[lp].tag + t[p].tag) % P;
	t[rp].tag = (t[rp].tag + t[p].tag) % P;
	t[p].tag = 0;
}

void update(int p, int l, int r, int L, int R, int z)
{
	if (l >= L && R >= r)
	{
		t[p].sum = (t[p].sum + t[p].len * z) % P;
		t[p].tag = (t[p].tag + z) % P;
		return;
	}
	pushdown(p);
	int mid = l + r >> 1;
	if (L <= mid)update(lson, L, R, z);
	if (R > mid)update(rson, L, R, z);
	t[p].sum = (t[lp].sum + t[rp].sum) % P;
}

ll query(int p, int l, int r, int L, int R)
{
	if (l >= L && R >= r)return t[p].sum;
	pushdown(p);
	int mid = l + r >> 1;
	ll ans = 0;
	if (L <= mid)ans = (ans + query(lson, L, R)) % P;
	if (R > mid)ans = (ans + query(rson, L, R)) % P;
	return ans;
}

//==================================================

int e[N], ne[N], h[N], idx;
int fa[N], dep[N], id[N], son[N], top[N], sz[N], cnt;

void add(int u, int v)
{
	e[++idx] = v;
	ne[idx] = h[u];
	h[u] = idx;
}

void dfs1(int fath, int u)
{
	fa[u] = fath;
	dep[u] = dep[fath] + 1;
	sz[u] = 1;
	for (int i = h[u]; i; i = ne[i])
	{
		int v = e[i];
		if (v == fath)continue;
		dfs1(u, v);
		sz[u] += sz[v];
		if (sz[v] > sz[son[u]])son[u] = v;
	}
}

void dfs2(int u, int ptop)
{
	id[u] = ++cnt;
	w[cnt] = wt[u];
	top[u] = ptop;
	if (!son[u])return;
	dfs2(son[u], ptop);
	for (int i = h[u]; i; i = ne[i])
	{
		int v = e[i];
		if (v != fa[u] && v != son[u])
			dfs2(v, v);
	}
}

//================================================

void op_1(int a, int b, int z)
{
	while (top[a] != top[b])
	{
		if (dep[top[a]] < dep[top[b]])swap(a, b);
		update(1, 1, n, id[top[a]], id[a], z);
		a = fa[top[a]];
	}
	if (dep[a] > dep[b])swap(a, b);
	update(1, 1, n, id[a], id[b], z);
}

ll op_2(int a, int b)
{
	ll ans = 0;
	while (top[a] != top[b])
	{
		if (dep[top[a]] < dep[top[b]])swap(a, b);
		ans = (ans + query(1, 1, n, id[top[a]], id[a])) % P;
		a = fa[top[a]];
	}
	if (dep[a] > dep[b])swap(a, b);
	ans = (ans + query(1, 1, n, id[a], id[b])) % P;
	return ans;
}

void op_3(int x, int z)
{
	update(1, 1, n, id[x], id[x] + sz[x] - 1, z);
}

ll op_4(int x)
{
	return query(1, 1, n, id[x], id[x] + sz[x] - 1);
}

int main()
{
	scanf("%d%d%d%lld", &n, &m, &r, &P);
	for (int i = 1; i <= n; i++)
		scanf("%d", &wt[i]);
	for (int i = 1; i < n; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		add(u, v), add(v, u);
	}
	dfs1(0, r);
	dfs2(r, r);
	build(1, 1, n);
	while (m--)
	{
		int op, x, y, z;
		scanf("%d", &op);
		switch (op)
		{
		case 1: scanf("%d%d%d", &x, &y, &z), op_1(x, y, z);			break;
		case 2: scanf("%d%d", &x, &y), printf("%lld\n", op_2(x, y));	break;
		case 3: scanf("%d%d", &x, &z), op_3(x, z);					break;
		case 4: scanf("%d", &x), printf("%lld\n", op_4(x));			break;
		}
	}
	return 0;
}

拓展

(21.5.12更新)

若查询修改的是边权而不是点权,步骤基本上差不多,只需要在dfs1()中将边权赋予儿子
拿上面的树砍掉一半作为示例,4条边的权值分别为 w 1 w_1 w1 w 2 w_2 w2 w 3 w_3 w3 w 4 w_4 w4,求节点8到节点7路上的权值之和,易知答案为 w 2 + w 3 + w 4 w_2+w_3+w_4 w2+w3+w4
求节点7到节点8路上的权值之和
将边权赋予儿子之后,在查询时注意不能直接搬查询点权的模板。
在查询点权的模板中,当两节点跳到同一条重链时会计算 w 1 + w 2 w_1+w_2 w1+w2,而实际上只能计算 w 2 w_2 w2
当两节点跳到同一条重链时,设这两个点分别为 u 、 v u、v uv,则深度较浅的必定为最开始时两节点的LCA,令这一节点为 u u u,我们可以显然可以知道只能计算 [ i d [ u ] + 1 , i d [ v ] ] [id[u]+1,id[v]] [id[u]+1,id[v]]间的权值。(当 u = v u=v u=v时不作处理)

参考文章:
https://www.cnblogs.com/chinhhh/p/7965433.html
https://oi-wiki.org/graph/hld/
https://www.cnblogs.com/ivanovcraft/p/9019090.html

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值