splay是平衡树的一种,但其更广泛的应用是进行对区间的操作,在熟悉了splay平衡树的一系列操作之后,就可以开始探究其区间操作了。
splay运用在区间操作上时,每个结点都对应区间上的一个点,每个子树都对应一段区间。而splay可以当作区间树的根本原因是“旋转操作不改变splay的中序遍历序列”,因此要对一段区间进行操作时,只需要通过一系列旋转,使某个子树对应该段区间即可。
这时候,满足二叉搜索树性质的是该结点对应的点在区间中的位置,但这个数据并不需要储存在splay树上。
首先考虑如何将一段区间对应到一个子树上。
如果要对区间
[
L
,
R
]
[L,R]
[L,R]进行操作,考虑先将对应
L
−
1
L-1
L−1的结点旋转到根,再将对应
R
+
1
R+1
R+1的结点旋转到根的右子结点,由于区间树是将区间的中序遍历顺序当作对应的结点在区间内的顺序,这时对应
R
+
1
R+1
R+1的结点的左子结点为根的子树对应的就是区间
[
L
,
R
]
[L,R]
[L,R]。
从上述操作可以发现,如果操作的区间是 [ 1 , k ] [1,k] [1,k]或者 [ k , N ] [k,N] [k,N],上述操作所要寻找的 L − 1 L-1 L−1结点或 R + 1 R+1 R+1结点可能不存在,因此如果可能会调用到 0 0 0结点的话需要插入一个哨兵结点,可能调用 N + 1 N+1 N+1结点也是同理在尾部插入哨兵结点。但是某些题目中,可能只会调用 0 0 0结点而不调用 N + 1 N+1 N+1结点,而又有向区间尾部合并的操作,这时在尾部插入了哨兵结点就可能会出现BUG,所以插入哨兵结点与否也要具体问题具体分析。
已经知道如何对应到区间上之后,就可以开始考虑一些基本的操作了。
对于区间翻转操作,完成了区间选取操作后可以给对应子树的根节点打上标记表示翻转操作,在输出或者旋转时swap左右子节点即可。
对于原splay的合并操作,如果子树B的最小值大于等于子树A的最大值,就可以将B合并到A上。但对于区间操作来说就没有这些限制,原来的合并操作等价于区间B合并到区间A之后。
分裂操作也是同理。
而对于区间最小值,区间增加一个数,可以类似线段树,在结点上记录数据并添加懒标记。
例如:POJ3580
题意:
共有六种操作:
①区间
[
L
,
R
]
[L,R]
[L,R]上均添加一个数
x
x
x
②翻转区间
[
L
,
R
]
[L,R]
[L,R]
③旋转区间
[
L
,
R
]
[L,R]
[L,R]
④在第
x
x
x个点后面插入值
p
p
p
⑤删除第
x
x
x个点
⑥求区间
[
L
,
R
]
[L,R]
[L,R]的最小值
AC代码
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
typedef double db;
typedef unsigned long long ull;
#define rep(i,s,t) for(int i=(s);i<=(t);i++)
#define per(i,s,t) for(int i=(s);i>=(t);i--)
#define ms0(ar) memset(ar,0,sizeof ar)
#define ms1(ar) memset(ar,-1,sizeof ar)
#define Ri(x) scanf("%d",&x)
#define Rii(x,y) scanf("%d%d",&x,&y)
#define Riii(x,y,z) scanf("%d%d%d",&x,&y,&z)
#define De(x) cout<<"#Debug "<<x<<'\n'
#define Dee(x,y) cout<<"#Debug "<<x<<' '<<y<<'\n'
#define pb push_back
//------
#define val(x) tr[x].val
#define fa(x) tr[x].fa
#define ch(x,d) tr[x].ch[d]
#define siz(x) tr[x].siz
#define mk(x) tr[x].mk
#define add(x) tr[x].add
#define qmin(x) tr[x].qmin
const int N=3e5+5;
const ll INF=0x3f3f3f3f3f3f3f3f;
class Splaytree{ // 如果T了 注释掉一些函数中的splay
public: // 如果有新加的函数 并且T了 试着在新加的函数里添加splay
int tot,rt,sl,sr; // sl-有无前哨兵结点 sr-有无后哨兵结点
struct Node{
int fa,ch[2],siz;
ll val,add,qmin;
bool mk;
void init(int v,int f){val=qmin=v,fa=f,ch[0]=ch[1]=add=0,mk=0,siz=1;} // 结点初始化
void clear(){val=fa=ch[0]=ch[1]=siz=qmin=0,mk=0;} // 清空结点
}tr[N];
Splaytree():tot(0),rt(0){}
void init(){rt=tot=0;}
void pushup(int x){
siz(x)=siz(ch(x,0))+siz(ch(x,1))+1;
qmin(x)=val(x);
if(ch(x,0)) qmin(x)=min(qmin(ch(x,0)),qmin(x));
if(ch(x,1)) qmin(x)=min(qmin(ch(x,1)),qmin(x));
}
void pushdown(int x){
if(mk(x)){
swap(ch(x,0),ch(x,1));
if(ch(x,0)) mk(ch(x,0))^=1;
if(ch(x,1)) mk(ch(x,1))^=1;
mk(x)=0;
}
if(add(x)){
if(ch(x,0)&&val(ch(x,0))!=INF){
add(ch(x,0))+=add(x);
qmin(ch(x,0))+=add(x);
val(ch(x,0))+=add(x);
}
if(ch(x,1)&&val(ch(x,0))!=INF){
add(ch(x,1))+=add(x);
qmin(ch(x,1))+=add(x);
val(ch(x,1))+=add(x);
}
add(x)=0;
}
}
int dir(int x){return x==ch(fa(x),1);} // 方向
void rotate(int x){ // 旋转
int y=fa(x),z=fa(y),d=dir(x); // 父节点/父节点的父节点/方向
// if(z) pushdown(z); // 注意按顺序pushdown
// pushdown(y); // 如果没有其他操作 调用splay前会先调用find
// pushdown(x); // 而find中已经进行pushdown操作 因此这里可以省去
ch(y,d)=ch(x,d^1),fa(ch(x,d^1))=y; // x原儿子变成f的儿子
ch(x,d^1)=y,fa(y)=x; // 调换x与f的位置
fa(x)=z;
if(z) ch(z,y==ch(z,1))=x;
pushup(y);
pushup(x); // 更新子树大小小
}
void splay(int x,int k){
k=fa(k);
while(fa(x)!=k){
int f=fa(x);
if(fa(f)!=k) rotate(dir(x)==dir(f)?f:x); // 判断方向是否相同选择旋转的节点
rotate(x);
}
if(!k) rt=x; // 如果旋转到根则更新根结点编号
}
int create(ll v,int fa){ // 创建新结点
tr[++tot].init(v,fa);
return tot;
}
void clear(int x){tr[x].clear();} // 清空结点
void build(int l,int r,int x){ // 构造完全平衡splay
if(l>r) return;
int m=(l+r)>>1;
ch(x,m>=x)=m,fa(m)=x,siz(m)=1;
if(l==r) return;
build(l,m-1,m);
build(m+1,r,m);
pushup(m);
}
void buildtree(int n,bool l,bool r,ll *v){ // lr-是否创建前后哨兵
if(l) sl=1;
if(r) sr=1;
tot=n;
if(sl){
clear(n+1);
val(1)=INF,tot++;
}
if(sr){
clear(n+2);
val(n+2)=INF,tot++;
}
rep(i,1,n){
clear(i+sl);
val(i+sl)=qmin(i+sl)=v[i];
}
build(1,tot,0);
rt=(tot+1)>>1;
fa(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 reverse(int l,int r){ // [l,r]翻转
if(l>r) swap(l,r);
int L=find(rt,l-1+sl),R=find(rt,r+1+sl);
splay(L,rt);
splay(R,ch(L,1));
mk(ch(R,0))^=1;
}
void split(int pos,int k,int &A,int &B){ // 分裂pos-前k个归属A 其余归属B
A=find(pos,k);
splay(A,pos);
pushdown(A);
B=ch(A,1);
ch(A,1)=0;
fa(B)=0;
pushup(A);
}
int merge(int A,int B){ // 合并-合并B到A
int pos=find(A,siz(A));
splay(pos,A);
ch(pos,1)=B;
fa(B)=pos;
pushup(pos);
return pos;
}
void print(int pos,int l,int r){ // 打印pos为根的子树上结点编号在[l,r]内的中序遍历
if(!pos) return; // 目的是为了不输出哨兵结点
pushdown(pos);
print(ch(pos,0),l,r);
if(val(pos)!=INF) printf("%lld ",val(pos));
print(ch(pos,1),l,r);
}
void addi(int l,int r,ll x){
if(l>r) swap(l,r);
int L=find(rt,l-1+sl),R=find(rt,r+1+sl);
splay(L,rt);
splay(R,ch(L,1));
qmin(ch(R,0))+=x;
add(ch(R,0))+=x;
val(ch(R,0))+=x;
pushup(R);
pushup(rt);
}
void revolve(int l,int r,ll x){
if(l>r) swap(l,r);
x%=(r-l+1);
if(!x) return;
int A,B,C,D;
split(rt,l,A,B);
split(B,r-l+1,B,C);
split(B,r-l+1-x,B,D);
D=merge(D,B);
D=merge(D,C);
rt=merge(A,D);
}
void ins(int x,ll p){
int A,B;
split(rt,x+1,A,B);
ch(A,1)=create(p,A);
pushup(A);
rt=merge(A,B);
pushup(rt);
}
void delet(int x){
int A,B,C;
split(rt,x,A,B);
split(B,1,C,B);
rt=merge(A,B);
pushup(rt);
}
ll query(int l,int r){
if(l>r) swap(l,r);
int L=find(rt,l-1+sl),R=find(rt,r+1+sl);
splay(L,rt);
splay(R,ch(L,1));
return qmin(ch(R,0));
}
}T;
char s[10];
ll a[N];
int n,m;
void print(){
T.print(T.rt,2,n+1);
printf("\n");
}
int main(){
int l,r;
ll x;
Ri(n);
rep(i,1,n)
scanf("%lld",&a[i]);
T.buildtree(n,1,1,a);
Ri(m);
while(m--){
scanf("%s",s);
if(s[0]=='A'){
Rii(l,r);
scanf("%lld",&x);
T.addi(l,r,x);
// print();
}
else if(s[0]=='R'){
if(s[3]=='E'){
Rii(l,r);
T.reverse(l,r);
// print();
}
else{
Rii(l,r);
scanf("%lld",&x);
T.revolve(l,r,x);
// print();
}
}
else if(s[0]=='I'){
Ri(l);
scanf("%lld",&x);
T.ins(l,x);
// print();
}
else if(s[0]=='D'){
Ri(x);
T.delet(x);
// print();
}
else{
Rii(l,r);
printf("%lld\n",T.query(l,r));
}
}
return 0;
}
本文探讨了Splay树在区间操作中的应用,通过旋转操作使得特定区间对应于子树,强调了区间操作的灵活性,并详细解释了区间翻转、合并、分裂以及区间最小值等操作的实现。同时,文章提醒在处理区间边界时插入哨兵结点的注意事项,以及针对不同问题的策略选择。
324

被折叠的 条评论
为什么被折叠?



