(简记)线段树合并 & 分裂

线段树合并

适用范围

针对树形问题,当每一个节点都要开一个数组维护信息,且儿子到父亲的转移是相同位操作,可使用线段树合并。

注意:线段树合并一般适用于子树信息维护。

前置知识

权值线段树

定义:对一个序列 \([1,n]\) 进行建树,但是叶子节点表示该节点所对应的值出现的次数

基础应用如可持久化线段树板子,对于每加入一个新节点,找到其所对应的叶子结点并更新其到父亲的一条链,这样就有多个版本的线段树,分别对应每个区间 \([1,i]\)

动态开点线段树

定义:对于每个节点 \(p\),仅当访问其左右儿子时给左右儿子赋一个新的节点编号,而不是常规的 \(ls\leftarrow 2p,rs\leftarrow 2p+1\)

线段树合并及其相关证明

每道题需要维护的信息不一样,以例题为例:

P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并

这里每个节点开一个线段树维护每个救济粮种类对应的救济粮数量。

线段树合并的重点在于 merge 函数,如下:

int merge(int fa,int u,int l,int r){
	if(!fa||!u)return fa|u;
	if(l==r){
		t[fa].mx=t[fa].mx+t[u].mx;//这一行根据合并方法和维护信息而定
		return fa;
	}
	int mid=(l+r)>>1;
	t[fa].ls=merge(t[fa].ls,t[u].ls,l,mid);
	t[fa].rs=merge(t[fa].rs,t[u].rs,mid+1,r);
	pushup(fa);
	return fa;
}
...
...
rt[u]=merge(rt[u],rt[v],1,kin);

其在做的事情就是把 \(T_1,T_2\) 两棵线段树的信息按位合并,过程即是向下递归直到至少其中一个节点为空。因此,merge 函数并不增加新节点,而是当有节点为空时,直接返回另一个。

在最劣情况下,该合并时间复杂度可以达到 \(O(n\log{n})\),但是一般题目稍加分析可知远远达不到。这里的时间分析主要取决于两个线段树内部经过修改的节点个数。然而我们不能让时间复杂度停留在 \(O(\text{玄学})\)!所以让我们来仔细分析一下。

考虑两棵线段树的合并,一个节点被访问,当且仅当这个节点在两棵线段树中都被修改。线段树的一次修改操作为从根到叶子节点的一条链(或者对于区间修改,一颗部分子树,但是这样很可能无法保证复杂度),对于这条链我们进行分析。

合并时,我们需要更新两条链的公共部分,然后直接丢掉非公共的部分。假设我们要将树 \(T_2\) 合并到树 \(T_1\) 上,那么我们会丢掉 \(T_2\) 所有与 \(T_1\) 重叠的节点,把它们的信息累加到 \(T_1\) 相应节点上,然后保留 \(T_2\) 不重叠的节点。

我们发现,这样操作 \(T_2\) 的每个节点至多被访问一次。对于 \(T_2\) 的修改操作总共有 \(O(n)\) 次,修改节点总数 \(O(n\log n)\),总时间复杂度即为 \(O(n\log n)\)

其他部分并无区别,下面给出完整代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m;
int idx,head[N];
struct Edge{
	int v,next;
}adj[N<<1];
void ins(int x,int y){
	adj[++idx].v=y;
	adj[idx].next=head[x];
	head[x]=idx;
}
int f[N][21],dep[N];
void dfs(int u,int fa){
	dep[u]=dep[fa]+1;
	for(int i=head[u];i;i=adj[i].next){
		int v=adj[i].v;
		if(v==fa)continue;
		f[v][0]=u;
		for(int j=1;(1<<j)<=dep[u];j++)
			f[v][j]=f[f[v][j-1]][j-1];
		dfs(v,u);
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--)
		if(dep[f[x][i]]>=dep[y])x=f[x][i];
	if(x==y)return x;
	for(int i=20;i>=0;i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}
vector<int>rv[N];
int nd,rt[N],kin;
struct Tre{
	int ls,rs,mx;
}t[N*20];
void pushup(int p){
	t[p].mx=max(t[t[p].ls].mx,t[t[p].rs].mx);
}
int merge(int fa,int u,int l,int r){
	if(!fa||!u)return fa|u;
	if(l==r){
		t[fa].mx=t[fa].mx+t[u].mx;
		return fa;
	}
	int mid=(l+r)>>1;
	t[fa].ls=merge(t[fa].ls,t[u].ls,l,mid);
	t[fa].rs=merge(t[fa].rs,t[u].rs,mid+1,r);
	pushup(fa);
	return fa;
}
void update(int &p,int l,int r,int pos,int val){
	if(!p)p=++nd;
	if(l==r){t[p].mx+=val;return ;}
	int mid=(l+r)>>1;
	if(pos<=mid)update(t[p].ls,l,mid,pos,val);
	else update(t[p].rs,mid+1,r,pos,val);
	pushup(p);
}
int query(int p,int l,int r,int val){
	if(l==r)return l;
	int mid=(l+r)>>1;
	int ls=t[p].ls,rs=t[p].rs;
	if(t[ls].mx==val)return query(ls,l,mid,val);
	return query(rs,mid+1,r,val);
}
int ans[N];
void dp(int u,int fa){
	for(int i=head[u];i;i=adj[i].next){
		int v=adj[i].v;
		if(v==fa)continue;
		dp(v,u);
		rt[u]=merge(rt[u],rt[v],1,kin);
	}
	for(int i:rv[u]){
		if(i>0)update(rt[u],1,kin,i,1);
		else update(rt[u],1,kin,-i,-1);
	}
	ans[u]=(t[rt[u]].mx==0?0:query(rt[u],1,kin,t[rt[u]].mx));
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		ins(u,v);
		ins(v,u);
	}
	dfs(1,0);
	for(int i=1;i<=m;i++){
		int x,y,z;
		cin>>x>>y>>z;
		kin=max(kin,z);
		int pp=LCA(x,y);
		if(x==y){
			rv[x].push_back(z);
			rv[f[x][0]].push_back(-z);
		}
		else if(pp==x||pp==y){
			int g=x+y-pp;
			rv[g].push_back(z);
			rv[f[pp][0]].push_back(-z);
		}
		else {
			rv[x].push_back(z);
			rv[y].push_back(z);
			rv[pp].push_back(-z);
			rv[f[pp][0]].push_back(-z);
		}
	}
	dp(1,0);
	for(int i=1;i<=n;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

其他例题

P3224 [HNOI2012] 永无乡

并查集+线段树合并,具体思路不言而喻,而需要注意的是该题的输入方式,不能使用朴素快读或格式化输入输出,而是应该使用cincout。(被硬控1h。。。)

P1600 [NOIP 2016 提高组] 天天爱跑步

需要进行一些推导,考虑到一条路径对一个点有贡献,当且仅当 \(u\) 在路径 \(s\rightarrow t\) 上,且 \(dis(s,u)=w[u]\)

我们可以分类讨论一下路径 \(s\rightarrow t\) 的形态,令 \(L=LCA(s,t)\),发现当 \(u\) 在路径 \(s\rightarrow L\) 上时需要满足 \(dep[s]-dep[u]=w[u]\),即 \(dep[s]=dep[u]+w[u]\),右边可以视为常数项,加入 \(dep[s]\) 后查询即可。

\(u\) 在路径 \(L\rightarrow t\) 上,需要满足 \(dep[u]-dep[L]+dep[s]-dep[L]=w[u]\),即 \(dep[u]-w[u]=2*dep[L]-dep[s]\),对于上面两种情况分别每个节点维护一个线段树查询即可。

需要注意的是,第二颗线段树的值域为 \([-n,n]\)

P10241 [THUSC 2021] 白兰地厅的西瓜

这是一个树上 LIS 问题,显然可以在一个节点上开两棵线段树分别记以下标为结尾的单调上升/下降子序列的最大长度,然后答案计算的话使用这两个合并即可。如果 \(u\) 不在拐点路径的 LIS 中,可以采用 DSU on tree 的方式把所有可能转移中点在轻子树重遍历一遍,加上线段树合并的查询时间复杂度 \(O(n\log^2 n)\),足以通过本题数据。

然而还可以做到更优。我们不枚举中点,而是直接把记信息的变成一棵线段树,在两棵线段树合并的时候采用类似分治的思想计算答案,左儿子和右儿子分别贡献一个向根的上升子序列和一个向叶子的子序列(反过来也一样),这样就去掉了 DSU on tree,时间复杂度 \(O(n\log n)\)

主播太弱了,打的时候没想到可以去掉一个 \(\log\),这是双 $\log $ 做法的代码。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
int ncnt,n,a[N],lsh[N];
int cnt,rt1[N],rt2[N],ans;
struct node{int lc,rc,mx;}t[N<<6];
vector<int>G[N];
#define ls t[p].lc
#define rs t[p].rc
#define mid ((l+r)>>1)
int query(int p,int l,int r,int L,int R){
	if(!p||L>R)return 0;
	if(L<=l&&r<=R)return t[p].mx;
	int mx=0;
	if(L<=mid)mx=max(mx,query(ls,l,mid,L,R));
	if(R>mid)mx=max(mx,query(rs,mid+1,r,L,R));
	return mx;
}
void merge(int o,int &p,int l,int r){
	if(!o||!p){p=o|p;return ;}
	t[p].mx=max(t[o].mx,t[p].mx);
	if(l==r)return ;
	merge(t[o].lc,ls,l,mid);
	merge(t[o].rc,rs,mid+1,r);
}
void update(int &p,int l,int r,int pos,int v){
	if(!p)p=++ncnt;
	t[p].mx=max(t[p].mx,v);
	if(l==r){return ;}
	if(pos<=mid)update(ls,l,mid,pos,v);
	else update(rs,mid+1,r,pos,v);
}
int siz[N],son[N];
void dfs0(int u,int fa){
	siz[u]=1;
	for(int v:G[u]){
		if(v==fa)continue;
		dfs0(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])
			son[u]=v;
	}
}
set<int>s[N];
void dfs(int u,int fa){
	if(son[u]){
		dfs(son[u],u);
		rt1[u]=rt1[son[u]];
		rt2[u]=rt2[son[u]];
		swap(s[u],s[son[u]]);
	}
	s[u].insert(a[u]);
	update(rt1[u],1,cnt,a[u],query(rt1[u],1,cnt,1,a[u]-1)+1);
	update(rt2[u],1,cnt,a[u],query(rt2[u],1,cnt,a[u]+1,cnt)+1);
	for(int v:G[u]){
		if(v==fa||v==son[u])continue;
		dfs(v,u);
		for(int p:s[v]){
			ans=max(ans,query(rt1[u],1,cnt,1,p-1)+query(rt2[v],1,cnt,p,cnt));
			ans=max(ans,query(rt1[v],1,cnt,1,p)+query(rt2[u],1,cnt,p+1,cnt));
			s[u].insert(p);
		}
		s[v].clear();
		merge(rt1[v],rt1[u],1,cnt);
		merge(rt2[v],rt2[u],1,cnt);
		update(rt1[u],1,cnt,a[u],query(rt1[u],1,cnt,1,a[u]-1)+1);
		update(rt2[u],1,cnt,a[u],query(rt2[u],1,cnt,a[u]+1,cnt)+1);
	}
}
#undef ls
#undef rs
#undef mid
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i],lsh[++cnt]=a[i];
	sort(lsh+1,lsh+1+cnt);
	cnt=unique(lsh+1,lsh+1+cnt)-(lsh+1);
	for(int i=1;i<=n;i++)
		a[i]=lower_bound(lsh+1,lsh+1+cnt,a[i])-lsh;
	for(int i=1;i<n;i++){
		int u,v;cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs0(1,0);
	dfs(1,0);
	cout<<max(max(t[rt1[1]].mx,t[rt2[1]].mx),ans);
	return 0;
}

P5298 [PKUWC2018] Minimax

这也是一个线段树合并直接记录前后缀信息的例子。发现可以离散化,然后对于下标 \(i\) 即记录取到 \(i\) 的概率,主要难点在于合并过程如何处理,发现两棵树合并后 \(i\) 的概率与另一棵子树中权值 \(<\)\(>\) 它的节点的概率都有关系,具体来说,令 \(l\) 为左儿子,\(r\) 为右儿子,则有:

\[f_{u,j}=f_{l,j}\times(p_u\sum_{k=1}^{j-1}f_{r,k}+(1-p_u)\sum_{k=j+1}^m f_{r,k})+f_{r,j}\times(p_u\sum_{k=1}^{j-1}f_{l,k}+(1-p_u)\sum_{k=j+1}^m f_{l,k}) \]

然后在合并过程中分别记录一下 \(p\) 子树/ \(o\) 子树的前缀/后缀概率和,这样就可以获得一个传参比命还长的写法。需要注意下述几点:

  1. 其中一个节点不存在时,由于不一定时叶子,所以需要对节点加一个乘法 \(\texttt{tag}\) 并每次访问时 pushdown(包括合并(\(p,o\) 都要)、修改、查询答案都需要如此)。

  2. merge 递归解决时可能出现,左子树递归完后左边的 \(sum\) 已经改变了,需要提前把两棵子树的原始 \(sum\) 记录下来。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL MOD=998244353,inva=796898467;
const int N=3e5+5;
int n,fat[N],p[N],w[N],cson[N];
int lson[N],rson[N],rt[N];
int lsh[N],cnt,ncnt;
LL ans;
#define ls t[p].lc
#define rs t[p].rc
#define mid ((l+r)>>1)
struct Node{int lc,rc;LL sum,tg;}t[N<<5];
void pushup(int p){
	t[p].sum=t[ls].sum+t[rs].sum;
	if(t[p].sum>=MOD)t[p].sum-=MOD;
}
void mktag(int p,LL v){
	t[p].tg=t[p].tg*v%MOD;
	t[p].sum=t[p].sum*v%MOD;
}
void pushdown(int p){
	if(t[p].tg!=1){
		mktag(ls,t[p].tg);
		mktag(rs,t[p].tg);
		t[p].tg=1;
	}
}
void modify(int &p,int l,int r,int pos,int v){
	if(!p)t[p=++ncnt].tg=1;
	pushdown(p);
	if(l==r){t[p].sum=v;return ;}
	if(pos<=mid)modify(ls,l,mid,pos,v);
	else modify(rs,mid+1,r,pos,v);
	pushup(p);
}
void merge(int o,int &p,int l,int r,LL prep,LL sufp,LL preo,LL sufo,LL v){
	if(!o||!p){
		if(!o&&!p){p=0;return ;}
		else if(!o)t[p].sum=t[p].sum*(preo*v%MOD+sufo*(MOD+1-v)%MOD)%MOD,
			t[p].tg=t[p].tg*(preo*v%MOD+sufo*(MOD+1-v)%MOD)%MOD;
		else t[o].sum=t[o].sum*(prep*v%MOD+sufp*(MOD+1-v)%MOD)%MOD,
			t[o].tg=t[o].tg*(prep*v%MOD+sufp*(MOD+1-v)%MOD)%MOD;
		p=o|p;
		return ;
	}
	if(l==r){
		t[p].sum=t[p].sum*(preo*v%MOD+sufo*(MOD+1-v)%MOD)%MOD;
		t[o].sum=t[o].sum*(prep*v%MOD+sufp*(MOD+1-v)%MOD)%MOD;
		t[p].sum=(t[o].sum+t[p].sum)%MOD;
		return ;
	}
	pushdown(p);
	pushdown(o);
	LL psum=t[ls].sum,osum=t[t[o].lc].sum;
	merge(t[o].lc,ls,l,mid,
		prep,(sufp+t[rs].sum)%MOD,preo,(sufo+t[t[o].rc].sum)%MOD,v);
	merge(t[o].rc,rs,mid+1,r,
		(prep+psum)%MOD,sufp,(preo+osum)%MOD,sufo,v);
	pushup(p);
}
void getans(int p,int l,int r,int &k){
	if(!p||!t[p].sum)return ;
	if(l==r){
		k++;
		(ans+=1ll*k*lsh[l]%MOD*t[p].sum%MOD*t[p].sum%MOD)%=MOD;
		return ;
	}
	pushdown(p);
	getans(ls,l,mid,k);
	getans(rs,mid+1,r,k);
}
#undef ls
#undef rs
#undef mid
void dfs(int u){
	if(!cson[u])modify(rt[u],1,cnt,w[u],1);
	else if(cson[u]==1)dfs(lson[u]),rt[u]=rt[lson[u]];
	else dfs(lson[u]),dfs(rson[u]),
		merge(rt[lson[u]],rt[rson[u]],1,cnt,0,0,0,0,p[u]),
		rt[u]=rt[rson[u]];
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>fat[i],cson[fat[i]]++;
		if(!lson[fat[i]])lson[fat[i]]=i;
		else rson[fat[i]]=i;
	}
	for(int i=1;i<=n;i++){
		cin>>p[i];
		if(cson[i])p[i]=1ll*p[i]*inva%MOD;
		else w[i]=p[i],p[i]=0,lsh[++cnt]=w[i];
	}
	sort(lsh+1,lsh+1+cnt);
	cnt=unique(lsh+1,lsh+1+cnt)-(lsh+1);
	for(int i=1;i<=n;i++)
		if(!cson[i])
			w[i]=lower_bound(lsh+1,lsh+1+cnt,w[i])-lsh;
	int rk=0;
	dfs(1);getans(rt[1],1,cnt,rk);
	cout<<ans<<'\n';
	return 0;
}

李超线段树合并

CF932F Escape Through Leaf

这玩意我都不知道该放哪儿。一开始不知道怎么做,后面发现是个笛卡尔积题,处理merge的信息合并只需要先合并结构,再用modify暴力下传线段(直线)永久标记即可,总的来说也没多难,只是在时间复杂度证明这一块:

线段树合并可以证明是 \(O(n\log n)\) 的,对于 \(n\) 条线段(直线),每条最多被下方到 \(mxdep+1\) 的深度(约 \(\log n\))然后就会消失,那么更新的总下传时间就是 \(O(n\log n)\) 的。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL INF=1e16+5;
const int N=1e5+5,lim=1e5;
int n,a[N],b[N];
vector<int>G[N];
struct sg{LL k,b;}my[N];
int ncnt,root[N];
struct Node{
	int tg,lc,rc;
}t[N<<5];
LL gt(int id,int x){return my[id].k*x+my[id].b;}
#define ls t[p].lc
#define rs t[p].rc
#define mid ((l+r)>>1)
void modify(int &p,int l,int r,int val){
	if(!val)return ;
	if(!p)p=++ncnt;
	if(!t[p].tg){t[p].tg=val;return ;}
	if(gt(val,mid)<gt(t[p].tg,mid))swap(val,t[p].tg);
	if(l==r)return ;
	if(gt(val,l)<gt(t[p].tg,l))modify(ls,l,mid,val);
	if(gt(val,r)<gt(t[p].tg,r))modify(rs,mid+1,r,val);
}
LL query(int p,int l,int r,int pos){
	if(!p)return INF;
	if(l==r)return gt(t[p].tg,pos);
	LL res=gt(t[p].tg,pos);
	if(pos<=mid)res=min(res,query(ls,l,mid,pos));
	else res=min(res,query(rs,mid+1,r,pos));
	return res;
}
void merge(int o,int &p,int l,int r){
	if(!o||!p){p=o|p;return ;}
	if(l==r){
		modify(p,l,r,t[o].tg);
		return ;
	}
	merge(t[o].lc,ls,l,mid);
	merge(t[o].rc,rs,mid+1,r);
	modify(p,l,r,t[o].tg);
}
LL f[N];
void dfs(int u,int fa){
	for(int v:G[u]){
		if(v==fa)continue;
		dfs(v,u);
		merge(root[v],root[u],-lim,lim);
	}
	if(root[u])f[u]=query(root[u],-lim,lim,a[u]);
	my[u]=(sg){b[u],f[u]};
	modify(root[u],-lim,lim,u);
}
int main(){
	my[0].b=INF;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)
		scanf("%d",&b[i]);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1,0);
	for(int i=1;i<=n;i++)
		printf("%lld ",f[i]);
	return 0;
}

线段树分裂

P5494 【模板】线段树分裂

本质上是线段树合并的逆过程,其实现过程可以参考 FHQ-Treap 的 Split 函数,我们根据一个键值 \(pos\) 将线段树 \(T_1\) 在值域上裂开成 \([1,pos],[pos+1,n]\) 两个不同线段树 \(T_2,T_3\),可以粗暴地理解为把 \(T_1\) 分别复制给 \(T_2,T_3\),然后删掉 \(T_2\)\([pos+1,n]\) 上的信息,删掉 \(T_3\)\([1,pos]\) 上的信息。

void split(int &o,int &p,int l,int r,int pos){
	if(pos>=r){p=0;return ;}
	if(!pos||pos+1<=l){p=o;o=0;return ;}
	if(!o){p=0;return ;}
	t[p=++ncnt]=t[o];
	if(pos+1<=mid){
		split(t[o].lc,ls,l,mid,pos);
		rs=t[o].rc,t[o].rc=0;
	}
	else split(t[o].rc,rs,mid+1,r,pos),ls=0;
	pushup(p);pushup(o);
}

这是自己实现的比较丑的一个分裂。需要注意对于 \(T_2,T_3\) 有效区间共用的线段树节点我们仍需要新开一些点,然后分类讨论一下,看看能不能把 \(o\) 的右子树完整地挂在 \(p\) 上,注意这里把一棵子树移动到另一棵后需要删掉原来的子树。这里是把 \([1,pos]\) 都挂在了 \(o\) 上,\([pos+1,n]\) 挂在了 \(p\) 上。引用传递真是个好东西

因为我们已经证明过合并时间复杂度正确,且分裂是单侧递归,所以单次是 \(O(\log n)\) 的,总时空复杂度都是 \(O(n\log n)\) 级别的。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+5;
int n,qn,a[N],rt[N],now,ncnt;
#define ls t[p].lc
#define rs t[p].rc
#define mid ((l+r)>>1)
struct Node{int lc,rc;LL sum;}t[N<<5];
void pushup(int p){
	t[p].sum=t[ls].sum+t[rs].sum;
}
void build(int &p,int l,int r){
	if(!p)p=++ncnt;
	if(l==r){t[p].sum=a[l];return ;}
	build(ls,l,mid);
	build(rs,mid+1,r);
	pushup(p);
}
void split(int &o,int &p,int l,int r,int pos){
	if(pos>=r){p=0;return ;}
	if(!pos||pos+1<=l){p=o;o=0;return ;}
	if(!o){p=0;return ;}
	t[p=++ncnt]=t[o];
	if(pos+1<=mid){
		split(t[o].lc,ls,l,mid,pos);
		rs=t[o].rc,t[o].rc=0;
	}
	else split(t[o].rc,rs,mid+1,r,pos),ls=0;
	pushup(p);pushup(o);
}
void merge(int o,int &p,int l,int r){
	if(!o||!p){p=o|p;return ;}
	if(l==r){t[p].sum+=t[o].sum;return ;}
	merge(t[o].lc,ls,l,mid);
	merge(t[o].rc,rs,mid+1,r);
	pushup(p);
}
void modify(int &p,int l,int r,int pos,int v){
	if(!p)p=++ncnt;
	if(l==r){t[p].sum+=v;return ;}
	if(pos<=mid)modify(ls,l,mid,pos,v);
	else modify(rs,mid+1,r,pos,v);
	pushup(p);
}
LL query(int p,int l,int r,int L,int R){
	if(!p)return 0;
	if(L<=l&&r<=R)return t[p].sum;
	LL res=0;
	if(L<=mid)res+=query(ls,l,mid,L,R);
	if(R>mid)res+=query(rs,mid+1,r,L,R);
	return res;
}
int kth(int p,int l,int r,LL k){
	if(l==r)return l;
	if(k<=t[ls].sum)return kth(ls,l,mid,k);
	else if(k-t[ls].sum<=t[rs].sum)return kth(rs,mid+1,r,k-t[ls].sum);
	return -1;
}
#undef ls
#undef rs
#undef mid
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>qn;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	build(rt[now=1],1,n);
	while(qn--){
		int op,p;cin>>op>>p;
		if(!op){
			int l,r;cin>>l>>r;
			int temp=0;
			split(rt[p],rt[++now],1,n,l-1);
			split(rt[now],temp,1,n,r);
			merge(temp,rt[p],1,n);
		}
		else if(op==1){
			int t;cin>>t;
			merge(rt[t],rt[p],1,n);
			rt[t]=0;
		}
		else if(op==2){
			int pos,v;cin>>v>>pos;
			modify(rt[p],1,n,pos,v);
		}
		else if(op==3){
			int l,r;cin>>l>>r;
			cout<<query(rt[p],1,n,l,r)<<'\n';
		}
		else if(op==4){
			LL k;cin>>k;
			cout<<kth(rt[p],1,n,k)<<'\n';
		}
	}
	return 0;
}
posted @ 2025-04-24 14:46  TBSF_0207  阅读(25)  评论(0)    收藏  举报