近期学习了Splay数据结构,发现是个挺有趣的东西。对模板题做一些记录~
模板
#include<cstdio>
#include<cmath>
#include<string>
#include<queue>
#include<map>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
/*
Splay Tree算法:伸展树,自调整形式的二叉平衡树
*/
typedef long long ll;
const int maxn=1e6+1e5+10;
const int inf=1<<30;
const double pi=acos(-1.0);
const int Mod=1e9+7;
const double eps=1e-9;
int root=0,tot=1; //树根节点
int fa[maxn], ch[maxn][2];
int val[maxn]; //结点权值和
int cnt[maxn]; //cnt[i]: 与i结点具有相同值的结点个数
int size[maxn]; //左右子树的结点个数(包含值重复)
//快读快写
inline int read()
{
int x=0,f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void update(int x){
// 维护当前子树的结点个数
size[x]=size[ch[x][0]]+size[ch[x][1]]+cnt[x];
}
void rotate(int x){
// 将树的x结点旋转到其父节点位置
// y为x父节点,z为y父节点,k表示x是否为y的右儿子
int y=fa[x],z=fa[y],k=(ch[y][1]==x);
ch[z][ch[z][1]==y]=x;
fa[x]=z; //将x与y位置交换,更新z的记录
ch[y][k]=ch[x][k^1]; fa[ch[x][k^1]]=y; //更新y节点的父亲和儿子
fa[y]=x;
ch[x][k^1]=y; // 更新x的k相对儿子为y
update(y); update(x); //x和y的子树结点个数发生变化,先更新儿子结点y的
}
void splay(int x, int goal){
// 每次有新节点加入、删除或查询时,都将其旋转至根节点,这样可以保持BST的平衡。
// 将x旋转至成为goal的儿子结点
int y,z;
while(fa[x]!=goal){
y=fa[x], z=fa[y];
// 异或为0:x,y,z在共线的链上
if(z!=goal) ((ch[y][0]==x)^(ch[z][0]==y))?rotate(x):rotate(y);
rotate(x);
}
if(goal==0) root=x; //0是x的父亲,x为根节点
}
void Find(int x){
// 查找x的位置,并将其旋转到根节点; 默认一定存在结点值为x
int u=root;
if(!u) return; //空树
while(ch[u][x>val[u]]&& x!=val[u]) //儿子节点存在并且当前结点val不是x,才进入到下一层
u=ch[u][x>val[u]]; //跳转到儿子结点
splay(u, 0);
}
void insert(int x){
// 插入值为x的结点
int u=root, ff=0; //当前结点u,其父节点ff
while(u&&x!=val[u]){ //当前结点存在,且x不等于当前结点的值
ff=u; u=ch[u][x>val[u]]; //若x>val[u]搜索右儿子结点,否则搜索左儿子
}
if(u){ //存在值为x的结点u
cnt[u]++;
}else{
u=tot++; //增加新的结点编号
if(ff) ch[ff][x>val[ff]]=u; //更新其父节点的信息
ch[u][0]=ch[u][1]=0; fa[u]=ff;
val[u]=x;
size[u]=1; cnt[u]=1;
}
// 把当前位置移到根,保证结构的平衡。注意前面因为更改了子树大小,所以这里必须Splay上去进行pushup保证size的正确
splay(u, 0);
}
int pre(int x){
// 查找前驱结点
Find(x); //查找后,此时树根即为查询节点
// x值不在树上,其pre可能为root
if(val[root]<x) return root;
int u=ch[root][0]; //前驱在当前根结点左子树的最右端
if(!u) return -1;
while(ch[u][1]) u=ch[u][1]; //一直向右走
return u;
}
int nxt(int x){
// 查找后继结点,同pre()类似
Find(x);
// x不在树上,其nxt可能为root
if(val[root]>x) return root;
int u=ch[root][1];
if(!u) return -1;
while(ch[u][0]) u=ch[u][0]; //一直向左走
return u;
}
void Delete(int x){
// 删除一个值为x的结点
// 以x的前驱pre作为根节点,以x的后继nxt作为pre的右儿子;此时x为nxt的左儿子且为叶子结点
int xp=pre(x), xn=nxt(x);
splay(xp, 0); splay(xn, xp);
int u=ch[xn][0]; //值为x要被删除的结点
if(cnt[u]>1){
cnt[u]--;
splay(u, 0); //将u旋转到根结点
}
else ch[xn][0]=0;
}
int kth(int k){
// 查找数值排名第k的结点编号
int u=root,son;
if(size[u]<k) return -1; //当前结点的结点个数小于k
while(true){
son=ch[u][0]; //左儿子
if(k<=size[son]) u=son; //进入左子树
else if(k<=size[son]+cnt[u]) return u; //排名为k的在当前结点
else k-=size[son]+cnt[u], u=ch[u][1]; // 进入右子树
}
}
int rank(int x){
Find(x); //将值为x的结点旋转到根
// 其排名即为左子树的size+1
if(val[root]>=x) return size[ch[root][0]]+1; //若当前根的值>=x,则排名为左子树大小+1
else if(val[root]<x) return size[ch[root][0]]+cnt[root]+1; //若当前根的值<x,则排名为左子树大小+根值的重复次数+1
}
int main(){
int N,m,op,x,lastans=0,res=0;
N=read(); m=read();
// 预先插入-inf和inf,保证删除最小和最大值时有前驱/后继
insert(-inf); insert(inf);
for(int i=1;i<=N;i++) x=read(), insert(x);
for(int i=1;i<=m;i++){
op=read(); x=read();
x^=lastans;
if(op==1) insert(x);
else if(op==2) Delete(x);
else if(op==3) insert(x), lastans=(rank(x)-1), Delete(x); //去掉-inf的排名
else if(op==4) lastans=val[kth(x+1)]; //不包含-inf的第k名
else if(op==5) insert(x),lastans=val[pre(x)],Delete(x);
else if(op==6) insert(x),lastans=val[nxt(x)],Delete(x);
if(op>=3){
res^=lastans;
// printf("%d\n",lastans);
}
}
// printf("inf: %d\n",inf);
printf("%d\n",res);
//
}
入门题目
洛谷P3391 文艺平衡树
洛谷P3391 文艺平衡树.
思路:
对于一棵树上连续的区间翻转,可以先令这一区间的结点在同一棵子树上。
翻转区间相当于翻转这棵子树,将其左右儿子交换并递归这一操作。
因此只需要将区间的前驱作为根,区间后继作为其右儿子,该区间就在右儿子的左子树上,
对该区间作一个翻转标记(类似lazy),
当走到这一子树时,下传标记并交换左右子树即可。
区间翻转也有应用于之后的题目。
洛谷P2234 营业额统计
洛谷P2234 营业额统计.
思路:很简单的题目,用stl也可做。
每次插入后查询其最近的前驱和后继,计算和它们差值绝对值最小值,加入res.
注意对于无前驱的情况,只能记录其与后继的差值;
同理无后继也类似处理。
洛谷P2596 [ZJOI2006]书架
思路:本质为维护一个排列
对每本书:
pos[x]:编号为x的书在Splay树的编号,val[x]:在Splay树中编号x的书的编号,
树中编号为x的书在书架的位置:size[左儿子]+1。
实现书的编号<->书在树中编号的双射。
1.对Top/Bottom操作:
(1) 将x映射到其在树中的编号x1,将x1作为树根
(2) 若x1无前驱,说明当前已经在书架顶部;若x1无后继,直接交换左右儿子;若都有则将左子树接到后继的左儿子,实现top操作。
对Bottom操作,与top类似。
2.对Insert操作,交换当前结点和前驱/后继即可
3.对Ask操作,找到x在树中编号x1,作为根节点,其左子树的size为其上方书的数量
4.对Query操作,按照kth函数查找到结点编号,输出书的编号即可。
ps: 输入输出不要搞混数值类型!( 输入输出小bug坑了两天
洛谷P1486 [NOI2004]郁闷的出纳员
思路:
对插入的每个工资,维护一个flag记录整体增/减数值,查询时减去flag
I x 插入操作: 若x<Min跳过;否则插入x+flag,记录插入个数.
S x 整体减去x操作:
(1)对flag+=x;
(2)维护splay树,将树中小于临界值Min+flag的工资去掉。具体为先insert(Min+flag),再删除其左子树和当前结点(注意预处理插入-inf)
A x 整体加上x操作: 对flag-=x
F x 查找排名第x大的工资: 利用kth()函数,但先看右子树再看左子树。
ps:
(1)delete后需更新父亲结点
(2)查找第k大需先看右子树再左子树,不能转换为第k小计算
P3224 [HNOI2012]永无乡
思路:
洛谷P3224 永无乡(Splay+并查集) 。
对于合并操作,并查集可以维护每个点所在的集合,
对于集合中数值的排序使用Splay Tree数据结构存储。
算法流程:
(1)合并x和y操作,先并查集查找fx和fy,
若在不在同一集合中,通过fx和fy定位到相应的splay树中,
将size较小的直接逐个结点插入到size较大的,修改f[fy]=fx,完成合并操作。
(2)查询x所在集合的第k大的编号,利用并查集得到fx,
对应到所在的splay树,利用kth函数查找树中第k大的结点编号。
ps:
(1)维护多条splay树,只需要存储每个根节点
(2)注意由编号->树结点编号->值的映射