树链剖分学习笔记
一、引入
前置知识:线段树,dfs序
给定一个n个节点的有根树,有m个操作;
该问题可以用 树链剖分 解决。
二、概念
树链剖分是将树划分为多个链从而利用数据结构维护的优雅暴力。
1. 重儿子(红色点)
一个节点的儿子节点中子树大小最大的节点。若有多个节点符合则取其一。若没有子节点则没有重儿子。
2. 轻儿子(黄色点)
一个节点的儿子节点中除重儿子的其他节点。
3. 重边
由父节点和重儿子组成的边。
4. 轻边
由父节点和轻儿子组成的边。
5. 重链(蓝色链)
由重边组成的链。
6. 轻链(黑色链)
由轻边组成的链。
三、原理
我们可以考虑处理出树上所有的重链,将节点重新标号使得一条重链上的节点序号连续;对于操作1,2的a,b两点可以分别从其所在的那条重链跳到他的上一条重链直到两点在同一条重链上,由于节点序号连续所以可以用线段树维护。对于操作3,4我们可以记录下一个节点
举个栗子:要求8到4最短路径上的节点之和。
核心思想:让两个点按深度大小,依次向上跳至上一条重链,直至处于同一条重链
- 发现8在5-8这条重链上,4在4-4这条重链上,即4本身。
- 比较两条链的起点:5号节点和4号节点的深度,5号节点更深,所以优先让8号节点沿着重链向上跳。
- 因为5-8重链上的节点重新标号之后序号连续,可以用线段树求出5-8重链的节点点值之和。
- 8号节点跳到所处重链的尽头5号节点之后接着找5号节点的父亲,发现是2号节点,2号节点在1-2-7-9-10这条重链上。
- 再次比较两条链的起点:1号节点和4号节点的深度,4号节点更深,所以让4号节点往上跳。
- 4号节点跳到1-2-7-9-10这条重链上之后,两个节点处在同一条重链上,最后计算1-2这条链的点值之和即可。
四、实现
dfs1及有关变量声明:处理节点的信息
ll dep[MAXN],fa[MAXN],siz[MAXN],son[MAXN];
//dep数组表示深度,fa数组表示父节点,siz数组表示该节点的子树大小,son数组表示该节点的重儿子。
void dfs1(int x,int f,int pdep){
dep[x]=pdep,fa[x]=f,siz[x]=1;
int wson=-1;
for(int i=0; i<G[x].size(); i++){//遍历儿子节点
int v=G[x][i];
if(v==f) continue;
dfs1(v,x,pdep+1);siz[x]+=siz[v];//计算该节点的子树大小
if(siz[v]>wson) wson=siz[v],son[x]=v;//处理重儿子
}
return;
}
dfs2及有关变量声明:处理链的信息
ll cnt,id[MAXN],trr[MAXN],top[MAXN];
//id数组表示重新编号后节点的序号,arr数组是读入的点的权值,trr是重新编号后的权值,top表示节点所处的链的起点。
void dfs2(int x,int chf){//chf是链的起点
id[x]=++cnt,trr[cnt]=arr[x],top[x]=chf;//重新编号,top[x]处理链
if(!son[x]){ return;}dfs2(son[x],chf);//访问重儿子,处理重链
for(int i=0; i<G[x].size(); i++){
int v=G[x][i];
if(v==fa[x] || v==son[x]) continue;//重儿子这条链在前面访问过了
dfs2(v,v);//处理轻链
}
return;
}
封装线段树,维护trr数组
struct Segment_Tree{
#define mid ((l+r)>>1)
ll arr[MAXN],tre[MAXN<<2],tag[MAXN<<2];
inline void build(int l,int r,int p){
if(l==r){tre[p]=arr[l];return;}
build(l,mid,p*2);build(mid+1,r,p*2+1);
tre[p]=tre[p*2]+tre[p*2+1];return;
}
inline void push_down(int l,int r,int p){
if(tag[p]==0) return;
tre[p*2]+=tag[p]*(mid-l+1),tre[p*2+1]+=tag[p]*(r-mid);
tag[p*2]+=tag[p],tag[p*2+1]+=tag[p];tag[p]=0;
return;
}
inline void add(int l,int r,int p,int a,int b,int k){
if(a<=l && r<=b){tre[p]+=k*(r-l+1),tag[p]+=k;return;}
push_down(l,r,p);
if(a<=mid) add(l,mid,p*2,a,b,k);
if(b>mid) add(mid+1,r,p*2+1,a,b,k);
tre[p]=tre[p*2]+tre[p*2+1];return;
}
inline ll query(int l,int r,int p,int a,int b){
if(a<=l && r<=b){return tre[p];}
ll temp=0;push_down(l,r,p);
if(a<=mid) temp+=query(l,mid,p*2,a,b);
if(b>mid) temp+=query(mid+1,r,p*2+1,a,b);
return temp;
}
}tret;
处理操作1
void ch_add(ll a,ll b,ll k){
while(top[a]!=top[b]){//当两点所处的重链不是一条时
if(dep[top[a]]<dep[top[b]]) swap(a,b);//使得a节点是深度较深的那个点(需要优先往上跳的点)
tret.add(1,n,1,id[top[a]],id[a],k);//在线段树上加,注意由于先访问的是链的顶端,所以线段树上加的区间应该是[id[top[a]],id[a]]
a=fa[top[a]];//将a节点向上跳
}
//现在两点处于同一条重链上
if(dep[a]>dep[b]) swap(a,b);//使得链中a节点在b点上方,这样重新标号后a序号更小
tret.add(1,n,1,id[a],id[b],k);
}
操作2与操作1同理
ll ch_query(ll a,ll b){
ll ans=0;
while(top[a]!=top[b]){
if(dep[top[a]]<dep[top[b]]) swap(a,b);
ans+=tret.query(1,n,1,id[top[a]],id[a]);
a=fa[top[a]];
}
if(dep[a]>dep[b]) swap(a,b);
ans+=tret.query(1,n,1,id[a],id[b]);
return ans;
}
处理操作3
void tre_add(ll r,ll k){
tret.add(1,n,1,id[r],id[r]+siz[r]-1,k);//该节点的子树重新标号后序号一定是连续的,所以直接加即可
}
操作4与操作3同理
ll tre_query(ll r){
return tret.query(1,n,1,id[r],id[r]+siz[r]-1);
}
AC代码
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
inline ll read(){
ll q=0;char ch=' ';
while(ch<'0' || ch>'9') ch=getchar();
while(ch<='9' && ch>='0') q=q*10+ch-'0',ch=getchar();
return q;
}
const int MAXN = 1e5+25;
ll n,m,s,p,arr[MAXN];
vector <int> G[MAXN];
ll dep[MAXN],fa[MAXN],siz[MAXN],son[MAXN];//dfs1: to dispose the point
ll cnt,id[MAXN],trr[MAXN],top[MAXN];//dfs2: to dispose the chain
struct Segment_Tree{//A segment tree to maintain the trr
#define mid ((l+r)>>1)
ll arr[MAXN],tre[MAXN<<2],tag[MAXN<<2];
inline void build(int l,int r,int p){
if(l==r){tre[p]=arr[l];return;}
build(l,mid,p*2);build(mid+1,r,p*2+1);
tre[p]=tre[p*2]+tre[p*2+1];return;
}
inline void push_down(int l,int r,int p){
if(tag[p]==0) return;
tre[p*2]+=tag[p]*(mid-l+1),tre[p*2+1]+=tag[p]*(r-mid);
tag[p*2]+=tag[p],tag[p*2+1]+=tag[p];tag[p]=0;
return;
}
inline void add(int l,int r,int p,int a,int b,int k){
if(a<=l && r<=b){tre[p]+=k*(r-l+1),tag[p]+=k;return;}
push_down(l,r,p);
if(a<=mid) add(l,mid,p*2,a,b,k);
if(b>mid) add(mid+1,r,p*2+1,a,b,k);
tre[p]=tre[p*2]+tre[p*2+1];return;
}
inline ll query(int l,int r,int p,int a,int b){
if(a<=l && r<=b){return tre[p];}
ll temp=0;push_down(l,r,p);
if(a<=mid) temp+=query(l,mid,p*2,a,b);
if(b>mid) temp+=query(mid+1,r,p*2+1,a,b);
return temp;
}
}tret;
void dfs1(int x,int f,int pdep){
dep[x]=pdep,fa[x]=f,siz[x]=1;
int wson=-1;
for(int i=0; i<G[x].size(); i++){
int v=G[x][i];
if(v==f) continue;
dfs1(v,x,pdep+1);siz[x]+=siz[v];
if(siz[v]>wson) wson=siz[v],son[x]=v;
}
return;
}
void dfs2(int x,int chf){
id[x]=++cnt,trr[cnt]=arr[x],top[x]=chf;
if(!son[x]){ return;}dfs2(son[x],chf);
for(int i=0; i<G[x].size(); i++){
int v=G[x][i];
if(v==fa[x] || v==son[x]) continue;
dfs2(v,v);
}
return;
}
void ch_add(ll a,ll b,ll k){
while(top[a]!=top[b]){
if(dep[top[a]]<dep[top[b]]) swap(a,b);
tret.add(1,n,1,id[top[a]],id[a],k);
a=fa[top[a]];
}
if(dep[a]>dep[b]) swap(a,b);
tret.add(1,n,1,id[a],id[b],k);
}
ll ch_query(ll a,ll b){
ll ans=0;
while(top[a]!=top[b]){
if(dep[top[a]]<dep[top[b]]) swap(a,b);
ans+=tret.query(1,n,1,id[top[a]],id[a]);
a=fa[top[a]];
}
if(dep[a]>dep[b]) swap(a,b);
ans+=tret.query(1,n,1,id[a],id[b]);
return ans;
}
void tre_add(ll r,ll k){
tret.add(1,n,1,id[r],id[r]+siz[r]-1,k);
}
ll tre_query(ll r){
return tret.query(1,n,1,id[r],id[r]+siz[r]-1);
}
signed main(){
n=read(),m=read(),s=read(),p=read();
for(int i=1; i<=n; i++) arr[i]=read();
for(int i=1; i<=n-1; i++){
int u=read(),v=read();
G[u].push_back(v);G[v].push_back(u);
}
dfs1(s,s,1);
dfs2(s,s);
for(int i=1; i<=n; i++) tret.arr[i]=trr[i];
tret.build(1,n,1);
for(int i=1; i<=m; i++){
ll op=read();
if(op==1){
ll l=read(),r=read(),k=read();ch_add(l,r,k);
}else if(op==2){
ll l=read(),r=read();printf("%lld\n",ch_query(l,r)%p);
}else if(op==3){
ll r=read(),k=read();tre_add(r,k);
}else{
ll r=read();printf("%lld\n",tre_query(r)%p);
}
}
}
五、习题
[HAOI2015]树上操作
裸的模板题。
[NOI2015] 软件包管理器
安装软件x即需要查询x到0号节点路径上未安装的软件数量;卸载软件x即查询x的子树重安装的软件数量。
线段树的写法稍有不同。节点为0表示未安装,1表示已安装,tag初始值为-1,当tag赋为0/1时表示将所维护区间的点值全部变为0/1。
[SDOI2011]染色
线段树每个节点多维护了两个值:lc,rc表示所维护区间的左右端点的颜色。查询时同时记录到查询区间的左右端点的颜色,向上跳时与上一层链的切入点的颜色对比,若颜色相同则
[国家集训队]旅游
码量巅峰
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】