洛谷P3384 树链剖分模板

博客介绍了如何使用树链剖分解决洛谷P3384问题,包括四种操作的支持:路径上节点值的修改与查询、子树值的修改与查询。详细解释了树链剖分的概念,如重儿子、轻儿子、重链、轻链等,并概述了两次DFS过程以及区间维护的方法。文章提到了区间修改和查询的实现策略,并建议结合图学习树链剖分,推荐了相关学习资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题意:

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

操作 1 : 1: 1 1 1 1 x x x y y y z z z 表示将树从 x x x y y y结点最短路径上所有节点的值都加上 z z z

操作 2 : 2: 2 2 2 2 x x x y y y 表示求树从 x x x y y y结点最短路径上所有节点的值之和

操作 3 : 3: 3 3 3 3 x x x z z z 表示将以 x x x为根节点的子树内所有节点值都加上 z z z

操作 4 : 4: 4 4 4 4 x x x 表示求以 x x x为根节点的子树内所有节点值之和

分析:

我的理解:

显然我们可以 l c a lca lca暴力求解,只不过会超时。
那么树链剖分登场了。
树链剖分将树的点分为轻点(轻儿子)和重点(重儿子),将树边分为轻边和重边,以及重链,再利用一些维护区间的数据结构有效的解决了这个问题。

重儿子:非叶节点中,这个节点的儿子中,子树节点最多的 那个儿子 称为重儿子。
轻儿子:不是重儿子就是轻儿子。
重边:父亲节点连接重儿子的边。
轻边:非重即轻。
重链:相邻重边连在一起形成的链。
轻链:非重即轻
对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链。
每一条重链以轻儿子为起点。

然后就说一下怎么实现吧(简述原理),
I . I. I. d f s 1 dfs1 dfs1第一次得到这棵树的重儿子、轻儿子、节点的父亲节点、节点的深度。

void dfs1(int x, int f, int d){
	pre[x] = f;
	deep[x] = d;
	size[x] = 1;
	int MAX = -1;
	for(int i = head[x]; i; i = edge[i].next){
		int v = edge[i].to;
		if(v == f) continue;
		dfs1(v, x, d + 1);
		size[x] += size[v];
		if(size[v] > MAX) MAX = size[v], son[x] = v;
	}
}

I I . II. II. d f s 2 dfs2 dfs2第二次得到映射的新点编号、新点的权值、每个点所在链的顶端那个点。

void dfs2(int x, int topf){
	id[x] = ++tot;
	nw[tot] = w[x];
	top[x] = topf;
	if(!son[x]) return ;
	dfs2(son[x], topf);
	for(int i = head[x]; i; i = edge[i].next){
		int v = edge[i].to;
		if(v == pre[x] || v == son[x]) continue;
		dfs2(v, v);
	}
}

d f s 2 dfs2 dfs2时我们 d f s dfs dfs的顺序是先重儿子再轻儿子,这样得到了可以用数据结构维护的区间。
因为我们是先重后轻,所以得到的重链一定是连续的,新子树的编号也是连续的(所以可以区间维护),这个可以自己画图比划一下, d f s dfs dfs而已。

怎么来维护任意两点的路径:
假设 x , y x,y x,y两点,我们令 x x x为链顶端那个点较深的那个点。
然后我认为和倍增求 l c a lca lca有点相似,
1. 1. 1.先处理 x x x x x x链顶端点的区间,
2. 2. 2.然后让 x x x点跳到 x x x链顶端点上面一个点,
3. 3. 3.反复,直至 x x x的链顶端点和 y y y相同。
4. 4. 4.现在 x x x y y y在一条链上,直接求他们的区间和即可。

w [ i ] w[i] w[i]表示 i i i节点权值
d e e p [ i ] deep[i] deep[i]表示 i i i节点深度
p r e [ i ] pre[i] pre[i]表示 i i i节点的父亲
t o p [ i ] top[i] top[i]表示 i i i节点的链顶端点
s o n [ i ] son[i] son[i]表示 i i i节点的重儿子
s i z e [ i ] size[i] size[i] i i i节点的子树节点个数
i d [ i ] id[i] id[i]表示 i i i节点的新编号
n w [ i ] nw[i] nw[i]表示 i i i新编号节点的权值

int queryDis(int x, int y){
	int ans = 0;
	while(top[x] != top[y]){
		if(deep[top[x]] < deep[top[y]]) swap(x, y);
		ans += query(1, 1, n, id[top[x]], id[x]);
		ans %= p;
		x = pre[top[x]];
	}
	if(deep[x] > deep[y]) swap(x, y);
	ans += query(1, 1, n, id[x], id[y]);
	ans %= p;
	return ans;
} 

区间修改怎么操作?和区间查询一模一样。

void updateDis(int x, int y, int z){
	while(top[x] != top[y]){
		if(deep[top[x]] < deep[top[y]]) swap(x, y);
		update(1, 1, n, id[top[x]], id[x], z);
		x = pre[top[x]];
	}
	if(deep[x] > deep[y]) swap(x, y);
	update(1, 1, n, id[x], id[y], z);
}

如何询问子树的和?我们知道子树的新编号是连续的,所以

int querySon(int x){
	return query(1, 1, n, id[x], id[x] + size[x] - 1) % p;
}

修改子树也是同理,

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

总的代码:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> pii;

const int maxn  = 2e5 + 5;
const int maxm  = 100 + 5;
const int inf   = 0x3f3f3f3f;
const LL  mod   = 1e9 + 7;//19260817
const double pi = acos(-1.0); 

int n, m, r, p, u, v, cnt, tot, opt, x, y, z;
int w[maxn], head[maxn]; 
int deep[maxn], pre[maxn], top[maxn], son[maxn], size[maxn], id[maxn], nw[maxn];//树剖  

struct qwq{
	int w, l, r, lazy;
	int len(){
		return r - l + 1;
	}
}a[maxn << 2];

struct node{
	int to, next;
}edge[maxn << 1];

void addedge(int u, int v){
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	head[u] = cnt;
}

void pushup(int k){
	a[k].w = (a[k << 1].w + a[k << 1 | 1].w) % p;
}

void pushdown(int k){
	if(a[k].lazy){
		a[k << 1].lazy += a[k].lazy;
		a[k << 1 | 1].lazy += a[k].lazy;
		a[k << 1].w += a[k << 1].len() * a[k].lazy;
		a[k << 1 | 1].w += a[k << 1 | 1].len() * a[k].lazy;
		a[k << 1].w %= p;
		a[k << 1 | 1].w %= p;
		a[k].lazy = 0;
	}
}

void build(int k, int l, int r){
	a[k].l = l, a[k].r = r;
	if(l == r){
		a[k].w = nw[l] % p;
		return ;
	}
	int mid = (l + r) >> 1;
	build(k << 1, l, mid);
	build(k << 1 | 1, mid + 1, r);
	pushup(k);
}

void update(int k, int l, int r, int x, int y, int z){
	if(l >= x && r <= y){
		a[k].lazy += z;
		a[k].w = (a[k].w % p + z * a[k].len() % p) % p;
		return ;
	}
	pushdown(k);
	int mid = (l + r) >> 1;
	if(x <= mid) update(k << 1, l, mid, x, y, z);
	if(y > mid) update(k << 1 | 1, mid + 1, r, x, y, z);
	pushup(k);
}

int query(int k, int l, int r, int x, int y){
	if(l >= x && r <= y){
		return a[k].w % p;
	}
	pushdown(k);
	int mid = (l + r) >> 1, ans = 0;
	if(x <= mid) ans = (ans + query(k << 1, l, mid, x, y)) % p;
	if(y > mid) ans = (ans + query(k << 1 | 1, mid + 1, r, x, y)) % p;
	return ans % p;
}

void dfs1(int x, int f, int d){
	pre[x] = f;
	deep[x] = d;
	size[x] = 1;
	int MAX = -1;
	for(int i = head[x]; i; i = edge[i].next){
		int v = edge[i].to;
		if(v == f) continue;
		dfs1(v, x, d + 1);
		size[x] += size[v];
		if(size[v] > MAX) MAX = size[v], son[x] = v;
	}
}

void dfs2(int x, int topf){
	id[x] = ++tot;
	nw[tot] = w[x];
	top[x] = topf;
	if(!son[x]) return ;
	dfs2(son[x], topf);
	for(int i = head[x]; i; i = edge[i].next){
		int v = edge[i].to;
		if(v == pre[x] || v == son[x]) continue;
		dfs2(v, v);
	}
}

int queryDis(int x, int y){
	int ans = 0;
	while(top[x] != top[y]){
		if(deep[top[x]] < deep[top[y]]) swap(x, y);
		ans += query(1, 1, n, id[top[x]], id[x]);
		ans %= p;
		x = pre[top[x]];
	}
	if(deep[x] > deep[y]) swap(x, y);
	ans += query(1, 1, n, id[x], id[y]);
	ans %= p;
	return ans;
} 

void updateDis(int x, int y, int z){
	while(top[x] != top[y]){
		if(deep[top[x]] < deep[top[y]]) swap(x, y);
		update(1, 1, n, id[top[x]], id[x], z);
		x = pre[top[x]];
	}
	if(deep[x] > deep[y]) swap(x, y);
	update(1, 1, n, id[x], id[y], z);
}

int querySon(int x){
	return query(1, 1, n, id[x], id[x] + size[x] - 1) % p;
}

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

int main(){
	scanf("%d %d %d %d", &n, &m, &r, &p);
	for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
	for(int i = 1; i <= n - 1; i++){
		scanf("%d %d", &u, &v);
		addedge(u, v), addedge(v, u);
	}
	dfs1(r, -1, 1);
	dfs2(r, r);
	build(1, 1, n);
	while(m--){
		scanf("%d", &opt);
		if(opt == 1){
			scanf("%d %d %d", &x, &y, &z);
			updateDis(x, y, z);
		}else if(opt == 2){
			scanf("%d %d", &x, &y);
			printf("%d\n", queryDis(x, y) % p);
		}else if(opt == 3){
			scanf("%d %d", &x, &z);
			updateSon(x, z);
		}else if(opt == 4){
			scanf("%d", &x);
			printf("%d\n", querySon(x) % p);
		}
	}
    return 0;
}

本人初学树链剖分,以上内容是我看了几篇博客自己的总结,有些借鉴。我讲的十分粗略,等我基本掌握了树剖的精髓会来继续更新博客。
我认为树链剖分得结合图来学,推荐几篇博客:
树链剖分详解
浅谈树链剖分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值