树链剖分——重链

根据雷母的建议,我觉得今天又要重新做人,开始分析制作各大板块的模板了!回忆了一下,感觉之前学的好多东西都忘记了。

前言:这篇博客纯属自己的理解,除了上课,完全没有查阅任何资料,如有错误,麻烦指出(没有图片,所以会很抽象,但重要的是模板hhhh)。


一.寻找重儿子

树链剖分是基于在线段树上的树上操作。

因为线段树只能对区间进行修改和查询,而如果是一棵树,那么我们就无法对它进行修改查询操作了。

所以我们就需要将这棵树搬到线段树上面去,也就是按照某种规律给这棵树重新编号,让这棵树成为一条一条连续的链,然后使它成为一个类似于区间的东西。这种编号的规律也就是寻找重儿子。

(1)重儿子

也就是一个结点的儿子中,孩子结点个数最大的那一个点。

而叶子节点没有重儿子。

(2)轻儿子

除了重儿子以外所有的子结点都是轻儿子。

那么我们的链都是以轻儿子开头的,以重儿子结尾的,这是我们建立区间遵循的规则 。

我曾经问过旁边的大佬为什么要这样建,它给出的想法是这样建立可以将时间复杂度尽量降低,也就是logn.


code:

void get_son(int x)
{
	siz[x] = 1 ;
	for(int i = 0; i < G[x].size(); i ++)
	{
		int y = G[x][i] ;
		if(y == fa[x]) continue ;
		dep[y] = dep[x] + 1 ;
		fa[y] = x ;
		get_son(y) ;
		siz[x] += siz[y] ;
		if(siz[y] > siz[son[x]]) son[x] = y ; 
	}
	return ;
}

 二.建立重链

我们根据规律,以根节点开头(根节点是轻儿子),然后寻找其重儿子,如果没有重儿子了,那么就往回寻找轻儿子,轻儿子可能是单独成链的,但是也有可能还有重儿子(水平不够,在讲废话)。

code:

void get_chain(int x, int tp)
{
	top[x] = tp ;//顶端的编号还是原来节点的编号 
	New[x] = ++ tot ;
	Past[tot] = x ;
	if(son[x]) get_chain(son[x], tp) ;
	for(int i = 0; i < G[x].size(); i ++)
	{
		int y = G[x][i] ; 
		if(y != son[x] && y != fa[x]) get_chain(y, y) ; //不能搜索已经搜过的这个节点的重儿子,也不能往上搜索
	}
	ctr[x] = tot ;//表示以x为根节点的子树中,编号最大的为ctr[x],所以这棵子树的编号是New[x] ~ ctr[x] 
}

 三.各种操作

由于Helioca太懒,只是想存个板子,其实这里就是线段树的代码,但是有一个重点就是我们的思维一定要明白树链剖分有什么用,以及为什么可以搬到线段树上面去。

例如我们寻找一个结点x到结点y的区间和,那么我们思考,x到y在新的线段树上的编号假设是a和b,区间查询只允许是连续的,不可能查找断开的区间,所以我们先查找x到top[x]他们在一条链上的和,在使x跳跃到top[x]的上一个结点,不断跳跃查找,而每次查找的x到top[x]一定都是一条链。最后x和y一定会在同一条链上,最坏的情况就是x和y都在以根节点为开头的链上,于是我们就可以直接找x到y的区间和了。这一切的操作都是为了维护线段树区间的连续性。

还有一点要区分的就是线段树的序号,和原始树的序号,在细节的地方一定要区分清楚,否则可能要调试很久的代码。

 


这里以洛谷的3384为例:

题目描述

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

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

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

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

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

输入格式

第一行包含 44 个正整数 N,M,R,PN,M,R,P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。

接下来一行包含 NN 个非负整数,分别依次表示各个节点上初始的数值。

接下来 N-1N−1 行每行包含两个整数 x,yx,y,表示点 xx 和点 yy 之间连有一条边(保证无环且连通)。

接下来 MM 行每行包含若干个正整数,每行表示一个操作。


输出格式

输出包含若干行,分别依次表示每个操作 22 或操作 44 所得的结果(对 PP 取模)。


输入输出样例

输入 #1复制

5 5 2 24
7 3 7 8 0 
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3

输出 #1复制

2
21

说明/提示

【数据规模】

对于 30\%30% 的数据: 1 \leq N \leq 101≤N≤10,1 \leq M \leq 101≤M≤10;

对于 70\%70% 的数据: 1 \leq N \leq {10}^31≤N≤103,1 \leq M \leq {10}^31≤M≤103;

对于 100\%100% 的数据: 1\le N \leq {10}^51≤N≤105,1\le M \leq {10}^51≤M≤105,1\le R\le N1≤R≤N,1\le P \le 2^{31}-11≤P≤231−1。


【样例说明】

树的结构如下:

各个操作如下:

故输出应依次为 22 和 2121。


#include <bits/stdc++.h>
#define ll long long 
using namespace std ;
const int MAXN = 500005 ;
int n, m, root, mod, a[MAXN], x, y, pd, z ;
int dep[MAXN], siz[MAXN], fa[MAXN], son[MAXN] ; //其中的编号都是原来节点的编号 
vector<int> G[MAXN] ;
struct Segement_tree{
	int l, r ;
	ll sum = 0, lazy ;
}t[MAXN * 4] ;
void get_son(int x)
{
	siz[x] = 1 ;
	for(int i = 0; i < G[x].size(); i ++)
	{
		int y = G[x][i] ;
		if(y == fa[x]) continue ;
		dep[y] = dep[x] + 1 ;
		fa[y] = x ;
		get_son(y) ;
		siz[x] += siz[y] ;
		if(siz[y] > siz[son[x]]) son[x] = y ; 
	}
	return ;
}
int top[MAXN], New[MAXN], Past[MAXN], tot = 0, ctr[MAXN] ;
void get_chain(int x, int tp)
{
	top[x] = tp ;//顶端的编号还是原来节点的编号 
	New[x] = ++ tot ;
	Past[tot] = x ;
	if(son[x]) get_chain(son[x], tp) ;
	for(int i = 0; i < G[x].size(); i ++)
	{
		int y = G[x][i] ; 
		if(y != son[x] && y != fa[x]) get_chain(y, y) ; //不能搜索已经搜过的这个节点的重儿子,也不能往上搜索
	}
	ctr[x] = tot ;//表示以x为根节点的子树中,编号最大的为ctr[x],所以这棵子树的编号是New[x] ~ ctr[x] 
}
void Build_tree(int p, int x, int y)
{
	t[p].l = x, t[p].r = y ;
	if(x == y){
		t[p].sum = a[Past[x]] ;
		return ;
	}
	int mid = (x + y) >> 1 ;
	Build_tree(p << 1, x, mid) ;
	Build_tree(p << 1 | 1, mid + 1, y) ;
	t[p].sum = t[p << 1].sum + t[p << 1 | 1].sum ; //这里需要Push_Up一下 
}
void Push_down(int p)//注意,这里是区间修改 
{
	if(t[p].lazy == 0 || t[p].l == t[p].r) return ;
	t[p << 1].sum += t[p].lazy * (t[p << 1].r - t[p << 1].l + 1) ;
	t[p << 1 | 1].sum += t[p].lazy * (t[p << 1 | 1].r - t[p << 1 | 1].l + 1) ;
	t[p << 1].lazy += t[p].lazy, t[p << 1 | 1].lazy += t[p].lazy ;
	t[p].lazy = 0 ; 
}
void Update(int p, int l, int r, int z)//注意l,r是固定的寻找区间 
{
	if(l <= t[p].l && t[p].r <= r){
		t[p].lazy += z ;
		t[p].sum += (t[p].r - t[p].l + 1) * z ;
		return ;
	}
	Push_down(p) ;
	int mid = (t[p].l + t[p].r) >> 1 ;//l~r是我们要寻找的区间
	if(l <= mid)
	{
		Update(p << 1, l, r, z) ;
	}
	if(r > mid)
	{
		Update(p << 1 | 1, l, r, z) ;
	}
	t[p].sum = t[p << 1].sum + t[p << 1 | 1].sum ;
	return ; 
} 
ll Sum(int p, int l, int r)
{
	if(l <= t[p].l && t[p].r <= r)
	{
		return t[p].sum ;
	}
	Push_down(p) ;
	ll ans = 0 ;
	int mid = (t[p].l + t[p].r) >> 1 ;
	if(l <= mid)
	{
		ans += Sum(p << 1, l, r) ;
	}
	if(r > mid)
	{
		ans += Sum(p << 1 | 1, l, r) ;
	}
	return ans ;
} 
void _1()
{
	scanf("%d%d%d", &x, &y, &z) ;
	while(top[x] != top[y])
	{
		if(dep[top[x]] > dep[top[y]]) swap(x, y) ;
		Update(1, New[top[y]], New[y], z) ;//往上面跳 
		y = fa[top[y]] ;//注意New[top[y]]已经计算过,所以这里计算的是top[y]的父亲 
	}
	if(dep[x] > dep[y]) swap(x, y) ;
	Update(1, New[x], New[y], z) ;
	return ;
} 
void _2()
{
	scanf("%d%d", &x, &y) ;
	ll ans = 0 ;
	while(top[x] != top[y])
	{
		if(dep[top[x]] > dep[top[y]]) swap(x, y) ;//那么y的深度比x的深度更大 
		ans = (ans + Sum(1, New[top[y]], New[y])) % mod ;
		//printf("%d %d!!!\n", top[y], y) ;
		//printf("%d %d!!!\n", New[top[y]], New[y]) ;
		y = fa[top[y]] ;
		//printf("%d!!!\n", ans) ;
	}
	if(dep[x] > dep[y]) swap(x, y) ;
	ans += Sum(1, New[x], New[y]) ;
	//printf("%d %d!!!\n", New[x], New[y]) ;
	printf("%lld\n", ans % mod) ;
	return ;
}
void _3()
{
	scanf("%d%d", &x, &z) ;
	Update(1, New[x], ctr[x], z) ;
}
void _4()
{
	scanf("%d", &x) ;
	printf("%lld\n", Sum(1, New[x], ctr[x]) % mod) ;
}
int main()
{
	scanf("%d%d%d%d", &n, &m, &root, &mod) ;
	for(int i = 1; i <= n; i ++)
	{
		scanf("%d", &a[i]) ;
	}
	for(int i = 1; i < n; i ++)
	{
		scanf("%d%d", &x, &y) ;
		G[x].push_back(y) ;
		G[y].push_back(x) ;	
	}
	get_son(root) ;
	get_chain(root, root) ;
	Build_tree(1, 1, n) ;
	for(int i = 1; i <= m; i ++)
	{
		scanf("%d", &pd) ;
		if(pd == 1) _1() ;
		if(pd == 2) _2() ;
		if(pd == 3) _3() ;
		if(pd == 4) _4() ;
	}
} 

谢谢观看,\(≧▽≦)/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值