目录
- 代码实现以及作为基础的平衡树的应用
- 在维护序列中的应用(重点)
- 提取区间
- 翻转区间
- 插入数到指定位置
@(目录)
代码实现以及作为基础的平衡树的应用
模板题
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
插入xx数
删除xx数(若有多个相同的数,因只删除一个)
查询xx数的排名(排名定义为比当前数小的数的个数+1+1。若有多个相同的数,因输出最小的排名)
查询排名为xx的数
求xx的前驱(前驱定义为小于xx,且最大的数)
求xx的后继(后继定义为大于xx,且最小的数)
先贴个代码
代码如下
#include
using namespace std;
#define ri register int
#define ll long long
const int N=200007;
int n;
templateinline void read(T &res){
static char son;T flag=1;
while((son=getchar())<'0'||son>'9') if(son=='-') flag=-1;
res=son-48;
while((son=getchar())>='0'&&son<='9') res=(res<<1)+(res<<3)+son-48;
res*=flag;
}
class Splay{
public:
int root,tot=0,fa[N],size[N],cnt[N],val[N],son[N][2];
private:
void update(int p){
size[p]=size[son[p][0]]+size[son[p][1]]+cnt[p];
}
int check(int p){
return p==son[fa[p]][1];
}
void rotate(int p){
int f=fa[p],ff=fa[f],k=check(p),kk=check(f),sp=son[p][k^1];
son[p][k^1]=f;fa[f]=p;
son[f][k]=sp;fa[sp]=f;
son[ff][kk]=p;fa[p]=ff;
update(f);update(p);
}
void splay(int p,int goal){
if(p==goal) return;//易忘
while(fa[p]!=goal){
int f=fa[p],ff=fa[f];
if(ff!=goal){
if(check(p)==check(f)) rotate(f);
else rotate(p);
}
rotate(p);
}
if(goal==0) root=p;//易忘
}//把p Splay到goal的儿子
void find(int x){
int cur=root;
while(son[cur][x>val[cur]]&&x!=val[cur])
cur=son[cur][x>val[cur]];
splay(cur,0);
}
public:
int rank(int x){
find(x);
return size[son[root][0]]+1;
}
void insert(int x){
int cur=root,p=0;//p是cur的父亲
while(cur&&x!=val[cur]){
p=cur;
cur=son[cur][x>val[cur]];
}
if(cur) ++cnt[cur];//找到了x
else{
cur=++tot;
son[cur][0]=son[cur][1]=0;
fa[cur]=p;
val[cur]=x;
cnt[cur]=size[cur]=1;//要赋全
if(p) son[p][x>val[p]]=cur;//一定要判断
}
splay(cur,0);
}
int pre(int x){
find(x);
if(val[root]x) return root;
int p=son[root][1];
while(son[p][0]) p=son[p][0];
return p;
}
void del(int x){
int pr=pre(x),nt=nxt(x);
splay(pr,0);splay(nt,pr);
int p=son[nt][0];
if(cnt[p]>1){
--cnt[p];
splay(p,0);
}
else son[nt][0]=0;
update(nt);update(root);
}
int search(int rk){
int p=root;
while(1){
if(son[p][0]&&rk<=size[son[p][0]]) p=son[p][0];//一定要判断是否有儿子
else if(rk>size[son[p][0]]+cnt[p]){
rk-=size[son[p][0]]+cnt[p];
p=son[p][1];//注意顺序
}
else return p;
}
}
}Tree;
int main()
{
Tree.insert(2147483647);
Tree.insert(-2147483647);//为了上下界减少讨论,我们插入两个标兵元素
read(n);
while(n--){
int opt,x;
read(opt);
read(x);
switch(opt){
case 1:{
Tree.insert(x);
break;
}
case 2:{
Tree.del(x);
break;
}
case 3:{
printf("%d\n",Tree.rank(x)-1);
break;
}
case 4:{
printf("%d\n",Tree.val[Tree.search(x+1)]);
break;
}
case 5:{
printf("%d\n",Tree.val[Tree.pre(x)]);
break;
}
case 6:{
printf("%d\n",Tree.val[Tree.nxt(x)]);
break;
}
}
}
return 0;
}
在维护序列中的应用(重点)
就像线段树既可以维护一个序列,在序列中完成各种灵活区间操作,也可以作为值域线段树,来维护具体的一些数值;
\(splay\)同样拥有维护一个序列的功能。
此时,\(splay\)中的排名不在是具体的数的排名,而是对应了一个数列的相应的位置,它具备很多优良的性质,我会列举几个常用的功能。
首先,我们要明确一件事,那就是我们维护的区间在什么地方。
根据平衡树的性质易得 我们的区间就保存在splay的中序遍历中(仔细思考),因为旋转不影响中序遍历。
提取区间
这是序列操作的基础(其实在某种程度上比线段树还好写)。
还记得删除操作吗?其实是一个道理的。例如我们要把区间\([l,r]\)提取出来,那么只需要把排名为\(l-1\)的点转到根节点,再把\(r+1\)转到根节点的儿子结点,根节点右边的就是区间\([l,n]\)(右边都比它大诶),\(r+1\)的左子树就是\([l,r]\)了。
刚接触时,我对于这些还是很疑惑的,为什么排名对应区间呢?排名不是根据权值调整的吗?
对于这个问题,我们要明白一件事,就是\(splay\)中排名实质上就是由我们插入的顺序决定的。回想上面的插入操作,我们过去在把\(splay\)作为普通平衡树使用时,只是刻意地把要插入的数调整到了相应位置,以此来保证我们插入的数保证平衡树的性质。
这里我把自己的问题分享出来,希望和大家共勉。
代码实现:
inline int gett(int l,int r){
l=search(l-1);
r=search(r+1);//找到排名对应的点的序号
splay(l,0);
splay(r,l);
return son[r][0];//区间就包含在r的左子树中
}
翻转区间
模板题
您需要写一种数据结构,来维护一个有序数列,其中需要提供以下操作:翻转一个区间,
例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1
翻转一个区间,实质就是把这个区间的所有左右子树交换位置。(试着手动模拟一下)
翻转一个指定的区间,我们只需要把这个区间先提取出来,再给这个子树的根节点打上一个翻转标记\(tag\)(参考线段树的标记思想)
最后在所有的操作完成后,对\(splay\)做一次中序遍历,把该下放的标记下放,最后输出中序遍历即可。
值得一提的是,我们在做旋转操作时,如果旋转了带有翻转标记的结点,那么结果会受到影响。为了避免这样的麻烦,我们就在\(search\)操作(根据排名找点)中,把碰到的每一个点的标记\(pushdown\),那么以后我们就可以为所欲为 地\(splay\)了。
理解后代码也很容易了
#include
using namespace std;
#define ri register int
const int N=200001;
int n,m;
templateinline void read(T &res){
static char ch;T flag=1;
while((ch=getchar())<'0'||ch>'9') if(ch=='-') flag=-1;
res=ch-48;
while((ch=getchar())>='0'&&ch<='9') res=(res<<1)+(res<<3)+ch-48;
res*=flag;
}
class Splay{
public:
int root=0,ndnum=0,son[N][2],fa[N],size[N],cnt[N],val[N];
bool tag[N];
inline void update(int p){
size[p]=size[son[p][0]]+size[son[p][1]]+cnt[p];
}
inline void pushdown(int p){
if(tag[p]){
if(son[p][0]) tag[son[p][0]]^=1;
if(son[p][1]) tag[son[p][1]]^=1;
swap(son[p][0],son[p][1]);
tag[p]^=1;
}
}
inline bool check(int p){
return p==son[fa[p]][1];
}
inline void rotate(int p){
int f=fa[p],ff=fa[f],k=check(p),kk=check(f),nd=son[p][k^1];
son[ff][kk]=p;fa[p]=ff;
son[p][k^1]=f;fa[f]=p;
son[f][k]=nd;fa[nd]=f;
update(f);update(p);
}
inline void splay(int p,int goal){
if(p==goal) return;
while(fa[p]!=goal){
int f=fa[p],ff=fa[f];
if(ff!=goal){
if(check(p)==check(f))
rotate(f);
else rotate(p);
}
rotate(p);
}
if(goal==0) root=p;
}
inline int search(int rk){
int p=root;
while(1){
pushdown(p);//标记下放
if(son[p][0]&&rk<=size[son[p][0]]) p=son[p][0];
else if(rk>size[son[p][0]]+cnt[p]){
rk-=size[son[p][0]]+cnt[p];
p=son[p][1];
}
else return p;
}
}
inline void insert(int x){
int f=0,cur=root;
while(cur&&x!=val[cur]){
f=cur;
cur=son[cur][x>val[cur]];
}
if(cur) ++cnt[cur];
else{
cur=++ndnum;
fa[cur]=f;
son[cur][0]=son[cur][1]=0;
size[cur]=cnt[cur]=1;
val[cur]=x;
if(f) son[f][x>val[f]]=cur;
}
splay(cur,0);
}
inline void reverse(int l,int r){
l=search(l);r=search(r+2);
splay(l,0);splay(r,l);
tag[son[r][0]]^=1;//区间在左儿子,打上标记,标记两次等于没标记
}
inline void bianli(int p){
pushdown(p);
if(son[p][0]) bianli(son[p][0]);
if(val[p]1)
printf("%d ",val[p]-1);
if(son[p][1]) bianli(son[p][1]);
}
}Tree;
int main()
{
read(n);read(m);
for(ri i=1;i<=n+2;++i) Tree.insert(i);//前后个多插入一个数防爆,注意范围的移动
for(ri i=1;i<=m;++i){
int l,r;
read(l);read(r);
Tree.reverse(l,r);
}
Tree.bianli(Tree.root);
return 0;
}
插入数到指定位置
这里只讲插入单个数,对于一段数的插入,有点麻烦 笔者水平有限
模板题
给定一个序列,初始为空。现在我们将1到N的数字插入到序列中,每次将一个数字插入到一个特定的位置。每插入一个数字,我们都想知道此时最长上升子序列长度是多少?
只讲插入操作,对于题目中求最长上升子序列请参考题解
把一个数\(x\)插入到位置\(pos\):把排名为\(pos-1\)的数转到根结点,把排名为\(pos\)的数转到根结点的儿子,显然\(pos\)的左子树为空,把\(x\)变为它的左儿子,即可完成操作。
代码实现:
inline void ins(int x,int pos){
int l=search(pos-1),r=search(pos);
splay(l,0);
splay(r,l);
son[r][0]=++ndnum;
fa[ndnum]=r;
cnt[ndnum]=size[ndnum]=1;
val[ndnum]=x;
splay(ndnum,0);
}