文章目录
前言
首先感谢洛谷大佬zengqinyi,受益匪浅,%%%
大佬详解链接:https://www.cnblogs.com/chinhhh/p/7965433.html
树链剖分
温馨提示
先把 LCA、树形DP、DFS序和线段树学了会方便理解一些。
概念及作用
树链剖分就是对一棵树分成几条链,把树形变为线性,减少处理难度。
重儿子和轻儿子
对于每一个非叶子节点,它的儿子中儿子数量最多的那一个儿子为该节点的重儿子,非重儿子的剩下所有儿子即为轻儿子。
叶子节点没有重儿子也没有轻儿子(因为它没儿子。。。)
重边和轻边
连接任意两个重儿子的边叫做重边,剩下的即为轻边。
重链
相邻重边连起来的连接一条重儿子的链叫重链。每一条重链以轻儿子为起点。
对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链。
【模板】重链剖分/树链剖分
参考题目:洛谷P3384 【模板】重链剖分/树链剖分
原题链接:https://www.luogu.com.cn/problem/P3384
题目
题目描述
如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
1 x y z,表示将树从 x 到 y 结点最短路径上所有节点的值都加上 z。
2 x y,表示求树从 x 到 y 结点最短路径上所有节点的值之和。
3 x z,表示将以 x 为根节点的子树内所有节点值都加上 z。
4 x 表示求以 x 为根节点的子树内所有节点值之和。
输入格式
第一行包含 4 个正整数 N,M,R,P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。
接下来一行包含 N 个非负整数,分别依次表示各个节点上初始的数值。
接下来 N−1 行每行包含两个整数 x,y,表示点 x 和点 y 之间连有一条边(保证无环且连通)。
接下来 M 行每行包含若干个正整数,每行表示一个操作。
输出格式
输出包含若干行,分别依次表示每个操作 2 或操作 4 所得的结果(对 P 取模)。
输入输出样例
输入 #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
题解
dfs1()
作用:
1.标记每个点的深度(dp[]数组).
2.标记每个点的父亲(fa[]数组)
3.标记每个非叶子节点的子树大小(含它自己)(size[]数组)
4.标记每个非叶子节点的重儿子编号(son[]数组)
inline void dfs1(int x,int f,int dep)//x为当前节点,f为父亲,dep为深度
{
dp[x]=dep;//标记深度
fa[x]=f;//标记父亲
size[x]=1;//标记非叶子节点的子树大小(含己)
int maxson=-1;//记录重儿子的儿子数
for(register int i=head[x];i;i=nex[i])
{
int y=to[x];
if(y==f) continue;//为父亲则continue
dfs1(y,x,dep+1);//dfs儿子
siz[x]+=siz[y];//加上儿子的儿子数
if(siz[y]>maxson)//标记非叶子节点的重儿子编号
{
son[x]=y,maxson=siz[y];
}
}
}
dfs2()
作用:
1.标记每个点的新编号(id[]数组)
2.赋值每个点的初始值到新编号上(初始值w[]数组,新编号值wt[]数组)
3.处理每个点所在链的顶端(top[]数组)
4.处理每条链
inline void dfs2(int x,int topf)//x为当前节点,topf为当前链的最顶端的节点
{
id[x]=cnt++;//标记点的新编号
wt[cnt]=w[x];//赋值初始值到新编号上
top[x]=topf;//处理点所在链的顶端
if(!son[x]) return;//没儿子(只有叶子节点没重儿子)返回
dfs2(son[x],topf);//按先处理重儿子,再处理轻儿子的顺序递归处理
for(register int i=head[x];i;i+=nex[i])
{
int y=to[x];
if(y==fa[x]||y==son[x]) continue;
dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链
}
}
处理问题(Attention)
前面说dfs2的顺序是先处理重儿子再处理轻儿子
我们来模拟一下(图):
解释:因为顺序是先重再轻,所以每一条重链的新编号是连续的;因为是dfs,所以每一个子树的新编号也是连续的。
需要处理的问题
1.修改任意两点间路径上的点权
2.处理任意两点间路径上的点权和
3.修改一点及其子树的点权
4.处理一点及其子树的点权和
处理任意两点间路径
求点权和
设所在链顶端的深度更深的那个点为x点.
1.ans加上x点到x所在链顶端这一段区间的点权和.
2.把x跳到x所在链顶端的那个点的上面一个点.
不停执行这两个步骤,直到两个点处于一条链上,这时再加上此时两个点的区间和即可.
这时我们注意到,我们所要处理的所有区间均为连续编号(新编号),于是用线段树处理连续编号区间和。
每次查询的时间复杂度为O(log2n)。
inline int qRange(int x,int y)
{
int ans=0;
while(top[x]!=top[y])//x和y不在同一条链
{
if(dp[top[x]]<dp[top[y]]) swap(x,y);//把x改为深度更深的链的点
res=0;
query(1,1,n,id[top[x]],id[x]);//ans加上x点到链顶端的点权和
ans+=res;
ans%=mod;//题目要求取模
x=fa[top[x]];//把x跳到x所在链顶端节点的父亲
}//使x和y在同一条链上
if(dp[x]>dp[y]) swap(x,y);//把x改为深度更深的点
res=0;
query(1,1,n,id[x],id[y]);//加上两个点的区间点权和
ans+=res;
return ans%=mod;
}
修改点权
区间修改和区间查询差不多,用update同时记一下要加的值。
inline void updaRange(int x,int y,int k)//x,y为路径首尾,k为要加的值
{
while(top[x]!=top[y])//同上个代码块
{
if(dp[top[x]]<dp[top[y]]) swap(x,y);//同上个代码块
update(1,1,n,id[top[x]],i