Splay
Splay
Splay基本操作
Splay是一类二叉查找树,与其他平衡树相比,也是运用旋转保证复杂度
其最重要的操作便是\(rotate\) \(and\) \(spaly\)了
先来谈旋转,我们都知道,旋转是这样的
仔细观察后,我们会发现,旋转操作可以拆解为三步,设\(x\)是\(y\)的父亲,\(k\)表示\(y\)是\(x\)的儿子(左0右1),现在要将\(y\)旋转
- 将\(x\)的父节点的子节点从\(x\)改为\(y\)
- 将\(x\)的\(k\)儿子换成\(y\)的\(1-k\)儿子
- 将\(y\)的\(1-k\)儿子变成\(x\)
写成代码即为:
void rotate(int x){
int y=t[x].f,z=t[y].f,k=t[y].ch[1]==x;
t[y].ch[k]=t[x].ch[k^1];
t[t[x].ch[k^1]].f=y;
t[z].ch[t[z].ch[1]==y]=x;
t[x].f=z;
t[x].ch[k^1]=y;
t[y].f=x;
pushup(y);
pushup(x);//不要忘记旋转后更新信息
}
接着便是\(Splay\)操作,对于使用到的一个节点\(x\),我们将其一直旋转至树根,便是\(Splay\)操作,在旋转过程中,不能一直单旋转,我们发现\(x和father(x)\)都是自己父亲的\(k\)儿子的情况下,先旋转\(x\)的父节点,再旋转\(x\)会使得树变得更加均衡,会更加优秀
void splay(int x,int goal=0){
while(t[x].f!=goal){
int y=t[x].f,z=t[y].f;
if(z!=goal){
t[z].ch[1]==y ^ t[y].ch[1]==x?rotate(x):rotate(y);
}
rotate(x);
}
if(!goal)root=x;
}
然后便是其他平衡树的常规操作了
- 插入 \(x\) 数
- 删除 \(x\) 数(若有多个相同的数,因只删除一个)
- 查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 \(+1\) )
- 查询排名为 \(x\) 的数
- 求 \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)
- 求 \(x\) 的后继(后继定义为大于 \(x\),且最小的数)
我们来分别考虑几种操作,为了方便操作,我们往树中插入两个无穷大和无穷小的哨兵
1.插入操作
在树中查找\(x\),并记录其父节点\(p\),若找到了\(x\)节点,则将其计数的\(cnt\)加上1,若没有找到\(x\)节点,此时\(p\)一定是一个叶子节点,直接判断\(x\)应是其的哪一个儿子,直接加上关系即可,最后还得\(Splay\)一下
void insert(int x){
int u=root,p=0;
while(u&&t[u].val!=x){
p=u;
u=t[u].ch[x>t[u].val];
}
if(!u){
u=++tot;
t[u].cnt=t[u].siz=1;
t[u].f=p,t[u].val=x;
if(p)t[p].ch[x>t[p].val]=tot;
}
else{
t[u].cnt++;
}
splay(u);
}
2.删除操作
因为普普通通的删除很麻烦,所以我们采用一种简单的方式:
将\(x\)在树中的严格前驱和严格后继找到,将严格前驱旋转到树根,将严格后继旋转为严格前驱的儿子,此时\(x\)一定位于\(t[t[root].ch[1]].ch[0]\),执行即可
void Delete(int x){
int last=nxt(x,0);
int nex=nxt(x,1);
splay(last,0);
splay(nex,last);
if(t[t[nex].ch[0]].cnt>1){t[t[nex].ch[0]].cnt--;splay(t[nex].ch[0]);}
else t[nex].ch[0]=0;
pushup(nex),pushup(last);
}
3.查询排名
我们可以对于每一个节点记录\(siz\),表示节点\(x\)及其子树中有多少个节点,这样我们就可以找到节点\(x\)后将其旋转至树根,求树根左儿子的\(siz\)再加一即可,但由于哨兵无穷小的存在,无需加一
int rak(int x){
find(x);//查找到x所在节点,并将其旋转至根节点
return t[t[root].ch[0]].siz;
}
4.查找排名为\(k\)的数
我们可以从根节点开始查找,对于一个节点\(p\),若\(k\le t[t[p].ch[0]].siz\),那么这个节点就在\(p\)的左子树中,若\(t[t[p].ch[0]].siz<k\le t[t[p].ch[0]].siz+t[p].cnt\),则说明当前节点\(p\)即为要找的节点,否则令\(k=k-t[p].cnt-t[t[p].ch[0]].siz\),然后再进入右子树
int kth(int u){
int x=root;
if(t[x].siz<u)return 0;
while(1){
if(ls&&u<=t[ls].siz){
x=ls;
}
else if(t[ls].siz+t[x].cnt<u){
u-=t[ls].siz+t[x].cnt;
x=rs;
}
else {splay(x);return t[x].val;}
}
}
5 and 6
前驱和后继的查找可以合成一段代码
\(x\)的前驱为\(x\)成为根节点之后,左子树中最大的(即左子树的最右子节点(一直往右跳即可))
后继类似
int nxt(int x,int f){
find(x);
if ((f==0&&t[root].val<x)||(f==1&&t[root].val>x))return root;//特判x不在树中的情况
int u=t[root].ch[f];
while (t[u].ch[!f])u=t[u].ch[!f];
splay(u);
return u;
}
还有就是\(find\)函数
void find(int x){
int u=root;
while(t[u].ch[x>t[u].val]&&t[u].val!=x){
u=t[u].ch[x>t[u].val];
}
splay(u);
}
序列之王-Splay的应用
Splay进行区间操作的原理就在于,其使用排名来构成区间下标,这样可以适应区间的增删
比如提取出区间\([l,r]\),我们仅需要将排名为\(l-1\)的节点旋转至根,再将排名为\(r+1\)的节点旋转至根的右儿子,此时根节点的右儿子的左儿子便是整个区间\([l,r]\),当然在实际应用中有哨兵这个恶心的玩意,于是我们需要对查询的排名进行\(+1\)
对于区间操作的Splay有一个好的建树方法,也即我们将\(a[1],a[n+2]\)(a表示原序列)设为哨兵,至于哨兵的值是0/-inf/inf需要根据具体情况而定,然后运用线段树的建树方式,建立出一个Splay,然后每次区间操作的时候都将\(l,r\)加上1
当然,Splay也有懒标记的扩展,但需要注意的是,对于懒标记的下传,一般有两种形式,一种是使用find函数下传,一种是在Splay函数中下传,这里我们使用find下传
对于Splay维护区间操作的核心函数在于两个:spilt and merge
spilt 分裂,merge合并
void splay(int x,int goal){//这里小小偷个懒
goal=f(goal);
while(f(x)!=goal){
int y=f(x),z=f(y);
if(z!=goal){
rc(y)==x^rc(z)==y?rotate(x):rotate(y);
}
rotate(x);
}
if(!goal)rt=x;
}
int find(int x,int k){ // 查询x的子树上第k大 ,递归写法
if(!x) return 0;
pushdown(x);
int s=siz(ch(x,0))+1;
if(s==k) return x;
else if(s>k) return find(ch(x,0),k);
else return find(ch(x,1),k-s);
}
void spilt(int x,int k,int &a,int &b){//将x中前k个分在a上,将其他的分在b上
a=find(x,k);
splay(a,x);
pushdown(a);//注意这里需要pushdown
b=rc(a);
rc(a)=0,f(b)=0;
pushup(a);
}
int merge(int a,int b){//将b合并在a上
int pos=find(a,siz(a));
splay(pos,a);
rc(pos)=b;
f(b)=pos;
pushup(pos);
return pos;
}//注意全部用pos
下面我们来两道例题感受一下Splay的强大,一般考察的区间操作也就这些了
超级备忘录
题目链接
描述
你的朋友达达被邀请参加一个叫做“超级备忘录”的电视节目。
在这个节目中,参与者需要玩一个记忆游戏。
在一开始,主持人会告诉所有参与者一个数列,\(A1,A2,…,An\)。
接下来,主持人会在数列上做一些操作,操作包括以下几种:
ADD x y D:给子序列 {Ax,…,Ay} 统一加上一个数 D。例如,在 {1,2,3,4,5} 上进行操作ADD 2 4 1 会得到 {1,3,4,5,5}。
REVERSE x y:将子序列 {Ax,…,Ay} 逆序排布。例如,在 {1,2,3,4,5} 上进行操作 REVERSE 2 4 会得到 {1,4,3,2,5}。
REVOLVE x y T:将子序列 {Ax,…,Ay} 轮换 T 次。例如,在 {1,2,3,4,5} 上进行操作 REVOLVE 2 4 2 会得到 {1,3,4,2,5}。
INSERT x P:在 Ax 后面插入 P。例如,在 {1,2,3,4,5} 上进行操作 INSERT 2 4 会得到 {1,2,4,3,4,5}。
DELETE x:删除 Ax。例如,在 {1,2,3,4,5} 上进行操作 DELETE 2 会得到 {1,3,4,5}。
MIN x y:询问子序列 {Ax,…,Ay} 中的最小值。例如,{1,2,3,4,5} 上执行 MIN 2 4 的正确答案为 2。
为了使得节目更加好看,每个参赛人都有机会在觉得困难时打电话请求场外观众的帮助。
你的任务是看这个电视节目,然后写一个程序对于每一个询问计算出结果,这样可以使得达达在任何时候打电话求助你的时候,你都可以给出正确答案。
下面我们来分析这道题目
首先对于ADD,我们找到这段区间,打上标记即可
void pushup(int x){
siz(x)=siz(lc(x))+siz(rc(x))+1;
mn(x)=val(x);
if(lc(x))mn(x)=min(mn(x),mn(lc(x)));
if(rc(x))mn(x)=min(mn(x),mn(rc(x)));
}
void addd(int l,int r,int x){
if(l>r)swap(l,r);
int pos1=find(rt,l),pos2=find(rt,r+2);//加入了哨兵
splay(pos1,rt),splay(pos2,rc(pos1));
int u=lc(pos2);
add(u)+=x,val(u)+=x,mn(u)+=x;
pushup(pos2);pushup(rt);
}
对于REVERSE,打上翻转标记,连左右儿子也不用翻(这个与1异或)
void pushrev(int x){
swap(lc(x),rc(x));
if(lc(x))rev(lc(x))^=1;
if(rc(x))rev(rc(x))^=1;
rev(x)=0;
}
void reverse(int l,int r){
if(l>r)swap(l,r);
int pos=find(rt,l);
int pos2=find(rt,r+2);
splay(pos,rt);
splay(pos2,rc(pos));
int u=lc(pos2);
rev(u)^=1;
}
对于REVOLVE,我们会发现这本质上是两端区间交换问题
void revolve(int l,int r,int t){
if(l>r)swap(l,r);
t%=(r-l+1);
if(!t)return ;
int a,b,c,d;
spilt(rt,l,a,b);
spilt(b,r-l+1,b,c);
spilt(b,r-l+1-t,b,d);//交换b,d
d=merge(d,b);
d=merge(d,c);
rt=merge(a,d);
}
对于INSERT只需要分裂一次合并两次即可
void ins(int x,int k){
int a,b;
spilt(rt,x+1,a,b);
++tot;
siz(tot)=1,f(tot)=a,lc(tot)=rc(tot)=add(tot)=rev(tot)=0,val(tot)=mn(tot)=k;
rc(a)=tot;
pushup(a);
rt=merge(a,b);
pushup(rt);
}
对于DELETE只需分裂两次合并一次即可
void Delete(int x){
int a,b,c;
spilt(rt,x,a,b);
spilt(b,1,c,b);
rt=merge(a,b);
pushup(rt);
}
贴上全部代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define int long long
struct node{
int ch[2],siz,rev,add,mn,val,fa;
}t[300005];
int rt,tot,a[400005];
#define INF 0x3f3f3f3f
#define lc(x) t[x].ch[0]
#define rc(x) t[x].ch[1]
#define siz(x) t[x].siz
#define rev(x) t[x].rev
#define mn(x) t[x].mn
#define val(x) t[x].val
#define f(x) t[x].fa
#define add(x) t[x].add
#define ch(x,y) t[x].ch[y]
void pushup(int x){
siz(x)=siz(lc(x))+siz(rc(x))+1;
mn(x)=val(x);
if(lc(x))mn(x)=min(mn(x),mn(lc(x)));
if(rc(x))mn(x)=min(mn(x),mn(rc(x)));
}
void pushrev(int x){
swap(lc(x),rc(x));
if(lc(x))rev(lc(x))^=1;
if(rc(x))rev(rc(x))^=1;
rev(x)=0;
}
void pushadd(int x,int c){
add(c)+=add(x);
mn(c)+=add(x);
val(c)+=add(x);
}
void pushdown(int x){
if(rev(x)){
pushrev(x);
}
if(add(x)){
if(lc(x))pushadd(x,lc(x));
if(rc(x))pushadd(x,rc(x));
add(x)=0;
}
}
void rotate(int x){
int y=t[x].fa,z=t[y].fa,k=t[y].ch[1]==x;
t[z].ch[t[z].ch[1]==y]=x;
t[x].fa=z;
t[y].ch[k]=t[x].ch[k^1];
t[t[x].ch[k^1]].fa=y;
t[x].ch[k^1]=y;
t[y].fa=x;
pushup(y);
pushup(x);
}
void splay(int x,int goal){
goal=f(goal);
while(f(x)!=goal){
int y=f(x),z=f(y);
if(z!=goal){
rc(y)==x^rc(z)==y?rotate(x):rotate(y);
}
rotate(x);
}
if(!goal)rt=x;
}
void build(int l,int r,int x){
if(l>r)return ;
int mid=l+r>>1;
t[mid].fa=x,t[x].ch[mid>=x]=mid,t[mid].siz=1;
build(l,mid-1,mid);build(mid+1,r,mid);
pushup(mid);
return ;
}
void build_tree(int n){
t[1].val=0x3f3f3f3f3f3f3f;
t[n+2].val=0x3f3f3f3f3f3f3f;
tot=n+2;
for(int i=1;i<=n;i++){
t[i+1].val=t[i+1].mn=a[i];
}
build(1,n+2,n+3>>1);
rt=n+3>>1;
f(rt)=0;
}
int find(int x,int k){ // 查询x的子树上第k大
if(!x) return 0;
pushdown(x);
int s=siz(ch(x,0))+1;
if(s==k) return x;
else if(s>k) return find(ch(x,0),k);
else return find(ch(x,1),k-s);
}
void spilt(int x,int k,int &a,int &b){
a=find(x,k);
splay(a,x);
pushdown(a);
b=rc(a);
rc(a)=0,f(b)=0;
pushup(a);
}
int merge(int a,int b){
int pos=find(a,siz(a));
splay(pos,a);
rc(pos)=b;
f(b)=pos;
pushup(pos);
return pos;
}
void reverse(int l,int r){
if(l>r)swap(l,r);
int pos=find(rt,l);
int pos2=find(rt,r+2);
splay(pos,rt);
splay(pos2,rc(pos));
int u=lc(pos2);
rev(u)^=1;
}
void addd(int l,int r,int x){
if(l>r)swap(l,r);
int pos1=find(rt,l),pos2=find(rt,r+2);
splay(pos1,rt),splay(pos2,rc(pos1));
int u=lc(pos2);
add(u)+=x,val(u)+=x,mn(u)+=x;
pushup(pos2);pushup(rt);
}
int find_min(int l,int r){
if(l>r)swap(l,r);
int pos1=find(rt,l),pos2=find(rt,r+2);
splay(pos1,rt),splay(pos2,rc(pos1));
return mn(lc(pos2));
}
void revolve(int l,int r,int t){
if(l>r)swap(l,r);
t%=(r-l+1);
if(!t)return ;
int a,b,c,d;
spilt(rt,l,a,b);
spilt(b,r-l+1,b,c);
spilt(b,r-l+1-t,b,d);
d=merge(d,b);
d=merge(d,c);
rt=merge(a,d);
}
void ins(int x,int k){
int a,b;
spilt(rt,x+1,a,b);
++tot;
siz(tot)=1,f(tot)=a,lc(tot)=rc(tot)=add(tot)=rev(tot)=0,val(tot)=mn(tot)=k;
rc(a)=tot;
pushup(a);
rt=merge(a,b);
pushup(rt);
}
void Delete(int x){
int a,b,c;
spilt(rt,x,a,b);
spilt(b,1,c,b);
rt=merge(a,b);
pushup(rt);
}
signed main(){
int n;
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
build_tree(n);
int m;
scanf("%lld",&m);
string a;
int l,r,d;
while(m--){
cin>>a;
if(a=="ADD"){
scanf("%lld%lld%lld",&l,&r,&d);
addd(l,r,d);
}
if(a=="REVERSE"){
scanf("%lld%lld",&l,&r);
reverse(l,r);
}
if(a=="REVOLVE"){
scanf("%lld%lld%lld",&l,&r,&d);
revolve(l,r,d);
}
if(a=="INSERT"){
scanf("%lld%lld",&l,&d);
ins(l,d);
}
if(a=="DELETE"){
scanf("%lld",&l);
Delete(l);
}
if(a=="MIN"){
scanf("%lld%lld",&l,&r);
printf("%lld\n",find_min(l,r));
}
}
}//细节巨多,小心谨慎
再来一道更加恐怖的题:
维护数列
请写一个程序,要求维护一个数列,支持以下 \(6\) 种操作:
编号 | 名称 | 格式 | 说明 |
---|---|---|---|
1 | 插入 | \(\operatorname{INSERT}\ posi \ tot \ c_1 \ c_2 \cdots c_{tot}\) | 在当前数列的第 \(posi\) 个数字后插入 \(tot\) 个数字:\(c_1, c_2 \cdots c_{tot}\);若在数列首插入,则 \(posi\) 为 \(0\) |
2 | 删除 | \(\operatorname{DELETE} \ posi \ tot\) | 从当前数列的第 \(posi\) 个数字开始连续删除 \(tot\) 个数字 |
3 | 修改 | \(\operatorname{MAKE-SAME} \ posi \ tot \ c\) | 从当前数列的第 \(posi\) 个数字开始的连续 \(tot\) 个数字统一修改为 \(c\) |
4 | 翻转 | \(\operatorname{REVERSE} \ posi \ tot\) | 取出从当前数列的第 \(posi\) 个数字开始的 \(tot\) 个数字,翻转后放入原来的位置 |
5 | 求和 | \(\operatorname{GET-SUM} \ posi \ tot\) | 计算从当前数列的第 \(posi\) 个数字开始的 \(tot\) 个数字的和并输出 |
6 | 求最大子列和 | \(\operatorname{MAX-SUM}\) | 求出当前数列中和最大的一段子列,并输出最大和 |
对于INSERT和DELETE,只需要在上道题的基础上稍加修改即可
对于MAKE-SAME,打一个赋值标记,并且清空翻转标记
对于REVERSE,同上题即可
对于GET-SUM,直接维护
对于MAX-SUM,仿照线段树方式处理
需要注意的是,本题卡空间,需要回收机制(使用栈或队列均可)
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#define rint register int
#define For(i,a,b) for (rint i=a;i<=b;++i)
using namespace std;
const int inf=0x3f3f3f3f;
const int N=1e6+17;
int n,m,rt,cnt;
int a[N],id[N],fa[N],c[N][2];
int sum[N],sz[N],v[N],mx[N],lx[N],rx[N];
bool tag[N],rev[N];
//tag表示是否有统一修改的标记,rev表示是否有统一翻转的标记
//sum表示这个点的子树中的权值和,v表示这个点的权值
queue<int> q;
inline int read(){
rint x=0,f=1;char ch=getchar();
while (ch<'0' || ch>'9'){if (ch=='-')f=-1;ch=getchar();}
while ('0'<=ch && ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x*f;
}
inline void pushup(rint x){
rint l=c[x][0],r=c[x][1];
sum[x]=sum[l]+sum[r]+v[x];
sz[x]=sz[l]+sz[r]+1;
mx[x]=max(mx[l],max(mx[r],rx[l]+v[x]+lx[r]));
lx[x]=max(lx[l],sum[l]+v[x]+lx[r]);
rx[x]=max(rx[r],sum[r]+v[x]+rx[l]);
}
//上传记录标记
inline void pushdown(rint x){
rint l=c[x][0],r=c[x][1];
if (tag[x]){
rev[x]=tag[x]=0;//我们有了一个统一修改的标记,再翻转就没有什么意义了
if (l)tag[l]=1,v[l]=v[x],sum[l]=v[x]*sz[l];
if (r)tag[r]=1,v[r]=v[x],sum[r]=v[x]*sz[r];
if (v[x]>=0){
if (l)lx[l]=rx[l]=mx[l]=sum[l];
if (r)lx[r]=rx[r]=mx[r]=sum[r];
}else{
if (l)lx[l]=rx[l]=0,mx[l]=v[x];
if (r)lx[r]=rx[r]=0,mx[r]=v[x];
}
}
if (rev[x]){
rev[x]=0;rev[l]^=1;rev[r]^=1;
swap(lx[l],rx[l]);swap(lx[r],rx[r]);
//注意,在翻转操作中,前后缀的最长上升子序列都反过来了,很容易错
swap(c[l][0],c[l][1]);swap(c[r][0],c[r][1]);
}
}
//下传标记
inline void rotate(rint x,rint &k){
rint y=fa[x],z=fa[y],l=(c[y][1]==x),r=l^1;
if (y==k)k=x;else c[z][c[z][1]==y]=x;
fa[c[x][r]]=y;fa[y]=x;fa[x]=z;
c[y][l]=c[x][r];c[x][r]=y;
pushup(y);pushup(x);
//旋转操作,一定要上传记录标记
}
inline void splay(rint x,rint &k){
while (x!=k){
int y=fa[x],z=fa[y];
if (y!=k){
if (c[z][0]==y ^ c[y][0]==x)rotate(x,k);
else rotate(y,k);
}
rotate(x,k);
}
}
//这是整个程序的核心之一,毕竟是伸展操作嘛
inline int find(rint x,rint rk){
pushdown(x);
//因为所有的操作都需要find,所以我们只需在这里下传标记就行了
rint l=c[x][0],r=c[x][1];
if (sz[l]+1==rk)return x;
if (sz[l]>=rk)return find(l,rk);
else return find(r,rk-sz[l]-1);
}
//这个find是我们整个程序的核心之二
//因为我们的区间翻转和插入及删除的操作的存在
//我们维护的区间的实际编号并不是连续的
//而,我们需要操作的区间又对应着平衡树的中序遍历中的那段区间
//所以这个find很重要
inline void recycle(rint x){
rint &l=c[x][0],&r=c[x][1];
if (l)recycle(l);
if (r)recycle(r);
q.push(x);
fa[x]=l=r=tag[x]=rev[x]=0;
}
//这就是用时间换空间的回收冗余编号机制,很好理解
inline int split(rint k,rint tot){
rint x=find(rt,k),y=find(rt,k+tot+1);
splay(x,rt);splay(y,c[x][1]);
return c[y][0];
}
//这个split操作是整个程序的核心之三
//我们通过这个split操作,找到[k+1,k+tot],并把k,和k+tot+1移到根和右儿子的位置
//然后我们返回了这个右儿子的左儿子,这就是我们需要操作的区间
inline void query(rint k,rint tot){
rint x=split(k,tot);
printf("%d\n",sum[x]);
}
inline void modify(rint k,rint tot,rint val){
rint x=split(k,tot),y=fa[x];
v[x]=val;tag[x]=1;sum[x]=sz[x]*val;
if (val>=0)lx[x]=rx[x]=mx[x]=sum[x];
else lx[x]=rx[x]=0,mx[x]=val;
pushup(y);pushup(fa[y]);
//每一步的修改操作,由于父子关系发生改变
//及记录标记发生改变,我们需要及时上传记录标记
}
inline void rever(rint k,rint tot){
rint x=split(k,tot),y=fa[x];
if (!tag[x]){
rev[x]^=1;
swap(c[x][0],c[x][1]);
swap(lx[x],rx[x]);
pushup(y);pushup(fa[y]);
}
//同上
}
inline void erase(rint k,rint tot){
rint x=split(k,tot),y=fa[x];
recycle(x);c[y][0]=0;
pushup(y);pushup(fa[y]);
//同上
}
inline void build(rint l,rint r,rint f){
rint mid=(l+r)>>1,now=id[mid],pre=id[f];
if (l==r){
mx[now]=sum[now]=a[l];
tag[now]=rev[now]=0;
//这里这个tag和rev的清0是必要,因为这个编号可能是之前冗余了
lx[now]=rx[now]=max(a[l],0);
sz[now]=1;
}
if (l<mid)build(l,mid-1,mid);
if (mid<r)build(mid+1,r,mid);
v[now]=a[mid]; fa[now]=pre;
pushup(now);
//上传记录标记
c[pre][mid>=f]=now;
//当mid>=f时,now是插入到又区间取了,所以c[pre][1]=now,当mid<f时同理
}
inline void insert(rint k,rint tot){
For(i,1,tot)a[i]=read();
For(i,1,tot)
if (!q.empty())id[i]=q.front(),q.pop();
else id[i]=++cnt;//利用队列中记录的冗余节点编号
build(1,tot,0);//将读入的tot个树建成一个平衡树
rint z=id[(1+tot)>>1];//取中点为根
rint x=find(rt,k+1),y=find(rt,k+2);
//首先,依据中序遍历,找到我们需要操作的区间的实际编号
splay(x,rt);splay(y,c[x][1]);
//把k+1(注意我们已经右移了一个单位)和(k+1)+1移到根和右儿子
fa[z]=y;c[y][0]=z;
//直接把需要插入的这个平衡树挂到右儿子的左儿子上去就好了
pushup(y);pushup(x);
//上传记录标记
}
//对于具体在哪里上传标记和下传标记
//可以这么记,只要用了split就要重新上传标记
//只有find中需要下传标记
//但其实,你多传几次是没有关系的,但是少传了就不行了
int main(){
n=read(),m=read();
mx[0]=a[1]=a[n+2]=-inf;
For(i,1,n)a[i+1]=read();
For(i,1,n+2)id[i]=i;//虚拟了两个节点1和n+2,然后把需要操作区间整体右移一个单位
build(1,n+2,0);//建树
rt=(n+3)>>1;cnt=n+2;//取最中间的为根
rint k,tot,val;char ch[10];
while (m--){
scanf("%s",ch);
if (ch[0]!='M' || ch[2]!='X') k=read(),tot=read();
if (ch[0]=='I')insert(k,tot);
if (ch[0]=='D')erase(k,tot);
if (ch[0]=='M'){
if (ch[2]=='X')printf("%d\n",mx[rt]);
else val=read(),modify(k,tot,val);
}
if (ch[0]=='R')rever(k,tot);
if (ch[0]=='G')query(k,tot);
}
return 0;
}