根据雷母的建议,我觉得今天又要重新做人,开始分析制作各大板块的模板了!回忆了一下,感觉之前学的好多东西都忘记了。
前言:这篇博客纯属自己的理解,除了上课,完全没有查阅任何资料,如有错误,麻烦指出(没有图片,所以会很抽象,但重要的是模板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() ;
}
}
谢谢观看,\(≧▽≦)/