树链剖分学习笔记

一、引入

前置知识:线段树,dfs序

洛谷P3384【模板】轻重链剖分/树链剖分

给定一个n个节点的有根树,有m个操作;
opt1: 将 a 节点到 b 节点的最短路径上的节点加上 k
opt2: 查询 a 节点到 b 节点的最短路径上节点之和
opt3: 将 x 节点的子树节点全部加上 k
opt4: 查询 x 节点的子树节点点值之和
该问题可以用 树链剖分 解决。

二、概念

树链剖分是将树划分为多个链从而利用数据结构维护的优雅暴力。

image

1. 重儿子(红色点)
一个节点的儿子节点中子树大小最大的节点。若有多个节点符合则取其一。若没有子节点则没有重儿子。
2. 轻儿子(黄色点)
一个节点的儿子节点中除重儿子的其他节点。
3. 重边
由父节点和重儿子组成的边。
4. 轻边
由父节点和轻儿子组成的边。
5. 重链(蓝色链)
由重边组成的链。
6. 轻链(黑色链)
由轻边组成的链。

三、原理

我们可以考虑处理出树上所有的重链,将节点重新标号使得一条重链上的节点序号连续;对于操作1,2的a,b两点可以分别从其所在的那条重链跳到他的上一条重链直到两点在同一条重链上,由于节点序号连续所以可以用线段树维护。对于操作3,4我们可以记录下一个节点 x 的子树大小 siz,该节点的子树序号一定是一段连续的区间 [x,x+siz1],同样可以用线段树维护。

image

举个栗子:要求8到4最短路径上的节点之和。
核心思想:让两个点按深度大小,依次向上跳至上一条重链,直至处于同一条重链

  1. 发现8在5-8这条重链上,4在4-4这条重链上,即4本身。
  2. 比较两条链的起点:5号节点和4号节点的深度,5号节点更深,所以优先让8号节点沿着重链向上跳
  3. 因为5-8重链上的节点重新标号之后序号连续,可以用线段树求出5-8重链的节点点值之和。
  4. 8号节点跳到所处重链的尽头5号节点之后接着找5号节点的父亲,发现是2号节点,2号节点在1-2-7-9-10这条重链上。
  5. 再次比较两条链的起点:1号节点和4号节点的深度,4号节点更深,所以让4号节点往上跳。
  6. 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表示所维护区间的左右端点的颜色。查询时同时记录到查询区间的左右端点的颜色,向上跳时与上一层链的切入点的颜色对比,若颜色相同则 ans -1。

[国家集训队]旅游
码量巅峰

image

posted @   smy2006  阅读(82)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示