P3391 【模板】文艺平衡树 题解
作者:互联网
描述:
给定 \(1\) 个长度为 \(n\) 的区间,有 \(m\) 次操作,每次反转区间 \([l,r]\) ,
求最后的序列。
思路:
区间操作,可以用平衡树中的 Splay 来解决。
不会平衡树的同学可以点这里:
-
下文默认会 Splay 的基础操作,
Rotate
,Splay
,\(0\) 表示左儿子, \(1\) 表示右儿子。
建树的时候,可以按照权值排序,也可以按照序列排序。
由于这里是区间反转,那么就要按照区间排序,也就是原序列排序。
Build
int Build(int l,int r,int fa){
int mid=(l+r)>>1,now=++cnt;//cnt节点编号+1
f[now]=fa;Val(now)=d[mid];
Son(now,0)=Son(now,1)=0;
if(l<=mid-1){Son(now,0)=Build(l,mid-1,now);}//递归左儿子
if(r>=mid+1){Son(now,1)=Build(mid+1,r,now);}//递归右儿子
Update(now);//更新
return now;
}
考虑如何 \(O(\log n)\) 进行区间反转操作。可以仿照线段树,进行标记。
由于 Splay 节点代表的不是区间,那问题就是如何把区间 \([l,r]\) 变到 \(1\) 棵子树上呢?
将 \(l-1\) 旋转到 \(rt\) ,将 \(r+1\) 旋转到 \(l-1\) 的右儿子,那么区间 \([l,r]\) 就是 \(r+1\) 的左儿子。
证明:
将 \(l-1\) 旋转到 \(rt\) ,那么 \([l,r]\) 必然在 \(l-1\) 的右儿子上。
又因为将 \(r+1\) 旋转到 \(rt\) 的右儿子,那么 \([l,r]\) 只可能是 \(r+1\) 的左儿子。
对应的操作:
Splay(l-1,0)
, Splay(r+1,0)
。
\(\mathtt{Ps:}\) 由于下文的代码会插入两个无用的节点,那么 l-1
和 r+1
会变成 l
和 r+2
。
\(r+1\) 是沿着祖先旋转的,那么如何保证 \(r+1\) 刚好旋转到 \(l-1\) 的右儿子呢?
因为在原树上, \(r+1\) 比 \(l-1\) 大,那么必定在 \(l-1\) 的右子树上,那么旋转就必然在 \(l-1\) 的右儿子了。
那将 \([l,r]\) 移到 \(1\) 棵子树上后, 就可以在子树的根上的 Tag
打上标记,如果后面有操作会改变该子树及其子孙的形态,那么就要先下传标记,这就在 Splay
中实现。
Update
void Update(int p){
Siz(p)=1;//当前节点
if(Son(p,0)){Siz(p)+=Siz(Son(p,0));}//加入左子树的节点
if(Son(p,1)){Siz(p)+=Siz(Son(p,1));}//加入右子树的节点
}
更新节点的信息。
P_d
void P_d(int p){
if(Tag(p)){
Tag(Son(p,0))^=1;//下传标记
Tag(Son(p,1))^=1;//下传标记
Tag(p)=0;//清空当前节点的标记
swap(Son(p,0),Son(p,1));//交换左右子树
}
}
意为 Push_down
,表示下传标记,将 Tag
当中的翻转标记下传给儿子,记住交换左右儿子且清空 Tag
。
Get
int Get(int p){
return Son(f[p],1)==p;//返回当前节点是其父亲的左儿子还是右儿子
}
Rotate
void Rotate(int p){
int fa=f[p],ffa=f[fa],m=Get(p),n=Get(fa);
Con(p,ffa,n);
Con(Son(p,m^1),fa,m);
Con(fa,p,m^1);
Update(fa);Update(p);//更新旋转后的当前节点和父亲节点
}
旋转操作。
Splay
void Splay(int x,int goal){
int len=0;
for(int i=x;i;i=f[i]){q[++len]=i;}
for(int i=len;i;--i){P_d(q[i]);}//下传标记
while(f[x]!=goal){
int fa=f[x];
if(f[fa]!=goal){
Rotate(Get(x)==Get(fa)?fa:x);
}
Rotate(x);
}
if(!goal){rt=x;}
}
第 \(2\) ~ \(4\) 行就是在有区间操作的时候,需要在 Splay
之前下传标记。
Fip
void Fip(int x,int y){
int l=Rank(x),r=Rank(y+2);
Splay(l,0);Splay(r,l);
Tag(Son(r,0))^=1;
}
由于插入了两个无用节点,kInf
和 -kInf
,保证 l-1
与 r+1
的存在,那么就要改变成 l
和 r+2
。
代码:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <ctime>
#define PR pair<int,int>
#define LL long long
using namespace std;
const int kN=1e5+5;
const int kInf=1e9;
struct Splay{
int son[2],val,siz;
bool tag;
#define Son(p,k) spy[p].son[k]
#define Tag(p) spy[p].tag
#define Val(p) spy[p].val
#define Siz(p) spy[p].siz
}spy[kN];
int n,m;
int d[kN],f[kN],q[kN];
int rt,cnt;
void Update(int p){
Siz(p)=1;//当前节点
if(Son(p,0)){Siz(p)+=Siz(Son(p,0));}//加入左子树的节点
if(Son(p,1)){Siz(p)+=Siz(Son(p,1));}//加入右子树的节点
}
int Build(int l,int r,int fa){
int mid=(l+r)>>1,now=++cnt;//cnt节点编号+1
f[now]=fa;Val(now)=d[mid];
Son(now,0)=Son(now,1)=0;
if(l<=mid-1){Son(now,0)=Build(l,mid-1,now);}//递归左儿子
if(r>=mid+1){Son(now,1)=Build(mid+1,r,now);}//递归右儿子
Update(now);//更新
return now;
}
void P_d(int p){
if(Tag(p)){
Tag(Son(p,0))^=1;//下传标记
Tag(Son(p,1))^=1;//下传标记
Tag(p)=0;//清空当前节点的标记
swap(Son(p,0),Son(p,1));//交换左右子树
}
}
int Get(int p){
return Son(f[p],1)==p;//返回当前节点是其父亲的左儿子还是右儿子
}
void Con(int x,int y,int z){
f[x]=y;
Son(y,z)=x;
}
void Rotate(int p){
int fa=f[p],ffa=f[fa],m=Get(p),n=Get(fa);
Con(p,ffa,n);
Con(Son(p,m^1),fa,m);
Con(fa,p,m^1);
Update(fa);Update(p);//更新旋转后的当前节点和父亲节点
}
void Splay(int x,int goal){
int len=0;
for(int i=x;i;i=f[i]){q[++len]=i;}
for(int i=len;i;--i){P_d(q[i]);}//下传标记
while(f[x]!=goal){
int fa=f[x];
if(f[fa]!=goal){
Rotate(Get(x)==Get(fa)?fa:x);
}
Rotate(x);
}
if(!goal){rt=x;}
}
int Rank(int k){
int now=rt;
while(1){
P_d(now);
if(Siz(Son(now,0))>=k){now=Son(now,0);}
else if(Siz(Son(now,0))+1==k){return now;}
else{
k-=Siz(Son(now,0))+1;
now=Son(now,1);
}
}
}
void Fip(int x,int y){
int l=Rank(x),r=Rank(y+2);
Splay(l,0);Splay(r,l);
Tag(Son(r,0))^=1;
}
void Out(int p){
P_d(p);
if(Son(p,0)){Out(Son(p,0));}
if(Val(p)>0&&Val(p)<n+1){printf("%d ",Val(p));}
if(Son(p,1)){Out(Son(p,1));}
}
int main(){
scanf("%d%d",&n,&m);
d[1]=-kInf;d[n+2]=kInf;
for(int i=2;i<=n+1;++i){d[i]=i-1;}
rt=Build(1,n+2,0);
for(int i=1;i<=m;++i){
int x,y;
scanf("%d%d",&x,&y);
Fip(x,y);
}
Out(rt);
return 0;
}
\(\mathtt{Ps:}\) 想学 Treap 的同学,可以点开上文的连接。
标签:fa,int,题解,Son,Splay,P3391,now,节点,模板 来源: https://www.cnblogs.com/Godzillad/p/15095585.html