线段树分裂

线段树分裂

顾名思义,就是把一颗线段树按照排名分成两棵树,通常会和线段树合并拌着吃。用官方一点的话来说,线段树分裂是线段树合并的逆运算,一般用于线段树维护的可重集,可以把一个可重集按照大小拆分成两个子集分别维护
注意当分裂和合并都存在时,我们在合并的时候必须回收节点,以避免分裂时会可能出现节点重复占用的问题

如何实现

想一想还学过什么数据结构依靠分裂和合并维护自身的值?
FHQ Treap !
没错,所以线段树合并与分裂一定程度上与FHQ Treap中的分裂合并操作比较相似
观察下面的分裂过程:
image
image
image
最左边的线段树即可按\([1,3]\)\([4,4]\)分裂成右边两颗线段树
考虑一个\(split\)函数,传入参数\(p,q,L,R\),代表把p上\([L,R]\)这一段区间分裂出来放到q上。首先我们容易知道,如果\(p=0\)也就是要分裂的节点是空节点,那啥也不用分了直接返回0。考虑用\(l,r\)维护当前递归到的区间,如果当前区间被目标区间完全包含,则直接把q赋值为p并清空p表示已经转移成功。如果目标区间不完全包含当前区间,就继续递归左右子树,进行分裂操作。简单来说,就是这么三步:
从 1 号结点开始递归分裂,当节点不存在或者代表的区间 [s,t] 与 [l,r] 没有交集时直接回溯
当 [s,t] 与 [l,r] 有交集时需要开一个新结点
当 [s,t] 包含于 [l,r] 时,需要将当前结点直接接到新的树下面,并把旧边断开

例题

P5494【模板】线段树分裂
五个操作,\(op_{0}\)\([x,y]\)分裂出来,直接\(split\)即可;\(op_{1}\)单次合并;\(op_{2}\)插入x个p单点修改即可,因为维护的是权值线段树;\(op_{3}\)\([x,y]\)中的数的个数,在权值线段树上区间求和;\(op_{4}\)查询区间第k大
代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 200010

int n,m;
int base[N*50];//初始新建的可重集中数字i的个数 
struct segtree{//动态开点权值线段树 
	int val,son[2];
}t[N*50];
int op,p,x,y;
int root[N*4];//存储根 
int rub[N*50];//回收站,存储被删除的点 
int cnt,tot;//cnt存储被删除节点的总数,tot存储新建节点的总数
int idx=1;//当前访问的根
 
/*内存分配与回收*/
//同时出现线段树分裂与合并时,需要进行节点的分配与回收,避免复用和冗余
int New(){
	//回收站里有点就复用,否则新建 
	return (cnt?rub[cnt--]:++tot);
} 

void del(int &p){
	//清空后放入回收站等待复用,并将被删结点和其父亲之间的关系解除 
	t[p].son[0]=t[p].son[1]=t[p].val=0;
	rub[++cnt]=p;
	p=0;
}

void push_up(int p){
	t[p].val=t[t[p].son[0]].val+t[t[p].son[1]].val;
}

/*建树*/
void build(int &p,int l,int r){
	if(!p) p=New();//没有就申请新内存
	if(l==r){
		cin>>t[p].val;//输入叶子节点权值 
		return;
		//叶子节点只记录这个数本身,其val代表这个数的出现次数 
	} 
	int mid=(l+r)>>1;
	build(t[p].son[0],l,mid);
	build(t[p].son[1],mid+1,r);
	push_up(p);
	return;
}

/*单点修改*/
//加入k个数x,相当于把点x的权值增加k 
void modify(int &p,int l,int r,int x,int k){
	if(!p) p=New();//如果没有就新建
	if(l==r){//找到x本身,直接更改 
		t[p].val+=k;
		return; 
	} 
	int mid=(l+r)>>1;
	//按照大小判断x属于左子树还是右子树,往对应方向寻找 
	if(x<=mid) modify(t[p].son[0],l,mid,x,k);
	else modify(t[p].son[1],mid+1,r,x,k);
	push_up(p);
	return;
}

/*线段树合并*/
//合并以p,q为根的两棵子树,并返回合并后的新根(此处将q并到p上) 
int merge(int p,int q,int l,int r){
	//if(!rt1&&!rt2) return 0;
	if(!p||!q) return p+q;
	if(l==r){
		t[p].val+=t[q].val;
		del(q);
		return p;
	}
	int mid=(l+r)>>1;
	t[p].son[0]=merge(t[p].son[0],t[q].son[0],l,mid);
	t[p].son[1]=merge(t[p].son[1],t[q].son[1],mid+1,r);
	push_up(p);
	del(q);
	return p;
}

/*线段树分裂*/
//把p包含的区间内,L到R的部分分裂到q代表的线段树中 
void split(int &p,int &q,int l,int r,int L,int R){
	if(R<l||r<L) return;//当前区间与目标区间没有交集,直接返回
	if(!p) return;//节点为空,无法分裂,返回 
	if(L<=l&&r<=R){
		//区间完全包含,直接赋值,然后把原线段树节点清空
		q=p;
		p=0;
		return; 
	}
	//如果目标节点为空,需要申请新节点
	if(!q) q=New();
	int mid=(l+r)>>1;
	//分别递归左右子树,进行分裂 
	if(l<=mid) split(t[p].son[0],t[q].son[0],l,mid,L,R);
	if(mid<r) split(t[p].son[1],t[q].son[1],mid+1,r,L,R); 
	//更新左右子树权值 
	push_up(p);
	push_up(q);
	return;
}

/*区间求和*/
//求区间l~r中大小处在L~R的有多少个数
int query(int p,int l,int r,int L,int R){ 
	if(!p) return 0;
	if(L<=l&&R>=r){
		return t[p].val;//完全包含直接返回 
	}
	int res=0; 
	int mid=(r+l)>>1;
	if(l<=mid) res+=query(t[p].son[0],l,mid,L,R);
	if(mid<r) res+=query(t[p].son[1],mid+1,r,L,R);
	return res;
} 

/*求区间第k大*/
int kth(int p,int l,int r,int k){
	if(l==r) return l;
	int mid=(l+r)>>1   ;
	int lft=t[t[p].son[0]].val;
	if(lft>=k) return kth(t[p].son[0],l,mid,k);
	else return kth(t[p].son[1],mid+1,r,k-lft);
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	build(root[1],1,n);
	for(int i=1;i<=m;i++){
		cin>>op;
		if(op==0){//分裂操作 
			cin>>p>>x>>y;
			split(root[p],root[++idx],1,n,x,y);
		}else if(op==1){
			cin>>p>>x;
			root[p]=merge(root[p],root[x],1,n);
		}else if(op==2){
			cin>>p>>x>>y;
			modify(root[p],1,n,y,x);
		}else if(op==3){
			cin>>p>>x>>y;
			cout<<query(root[p],1,n,x,y)<<'\n';
		}else if(op==4){
			cin>>p>>x;
			if(t[root[p]].val<x) cout<<-1<<"\n";
			else cout<<kth(root[p],1,n,x)<<"\n";
		}
	}
	return 0;
}

posted @ 2025-04-19 08:41  Yun_Mo_s5_013  阅读(9)  评论(0)    收藏  举报