Splay

Splay

Splay基本操作

Splay是一类二叉查找树,与其他平衡树相比,也是运用旋转保证复杂度
其最重要的操作便是\(rotate\) \(and\) \(spaly\)
先来谈旋转,我们都知道,旋转是这样的
在这里插入图片描述
仔细观察后,我们会发现,旋转操作可以拆解为三步,设\(x\)\(y\)的父亲,\(k\)表示\(y\)\(x\)的儿子(左0右1),现在要将\(y\)旋转

  1. \(x\)的父节点的子节点从\(x\)改为\(y\)
  2. \(x\)\(k\)儿子换成\(y\)\(1-k\)儿子
  3. \(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;
}

然后便是其他平衡树的常规操作了

  1. 插入 \(x\)
  2. 删除 \(x\) 数(若有多个相同的数,因只删除一个)
  3. 查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 \(+1\) )
  4. 查询排名为 \(x\) 的数
  5. \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)
  6. \(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;
}
posted @ 2022-11-30 22:29  spdarkle  阅读(47)  评论(0编辑  收藏  举报