越学越把treap , fhq - Treap和splay记混了,splay树在于其区间翻转等操作
文章目录
最新更新 平衡树硬板子(牛客维修数列)
传送门
----------------------------------------------------------- (分割线)
这里先把一些spla树的基本操作模板给一下
来补哨兵的坑了
这种删除操作需要找到前驱后继,但第一个点和最后一个点分别没有前驱和后继 所以需要添加±INF使每个点都有前驱后继
缺失哨兵也会导致
特别注意我们要先加虚点−inf和inf
在旋转中tree[tree[x].ch[k^1]].f = fa;
这一行代码会在 tree[x].ch[k^1] 不存在时不断改变0节点的fa值,所以当没有哨兵的时候
如果要删除最小或最大的节点,那么当旋转它们的前驱或者后继时,就等于要旋转0节点,而0节点的父亲节点这时候已经改变了,至少不为0了,所以不论怎么旋转,0节点的fa一定不为0了,也就导致会一直卡在splay这个函数里,
同时如果出现要找的数的前驱不存在,那么也会导致前驱指向0节点,这样又会出现上面旋转0节点的情况,从而导致死循环
这就是加哨兵的意义,确实能够减少很多越界的情况
不加的话会卡死在把前驱转到根节点的地方
我认为对于题目来说一般都是死循环出现在删除的时候,因为题目肯定不会让你求最小值的前驱或者最大值的后继的
例题(模板)洛谷3369
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <string>
#include <cmath>
#include <cstdlib>
using namespace std;
const int MAXN = 100005;
const int INF = 0x7f7f7f7f;
/********************
splay树
********************/
struct node
{
int f,cnt,val,size,ch[2];
//f为该节点的父节点(如果f为0,则表示该节点为根节点)
//cnt表示当前该点有多少个重复(包括自己)的
}tree[MAXN];
int root,Size,n;
//更新节点数
void update(int x)
{
tree[x].size = tree[tree[x].ch[0]].size + tree[tree[x].ch[1]].size + tree[x].cnt;
}
//旋转
void rotate(int x)
{
int fa = tree[x].f;
int gfa = tree[fa].f;
//判断x是父节点的哪个儿子,0为左,1为右
int k = (tree[fa].ch[1] == x);
//把x的父节点在gfa的位置用x取代
tree[gfa].ch[tree[gfa].ch[1] == fa] = x;
tree[x].f = gfa;//x的父亲节点变成了gfa
tree[fa].ch[k] = tree[x].ch[k^1];
tree[tree[x].ch[k^1]].f = fa;
tree[x].ch[k^1] = fa;
tree[fa].f = x;
update(fa);
update(x);
}
//splay操作,如果三点一线,且祖父节点不是目标节点,就先转fa再转x,不是三点
//一线,就都转x,一直转到x为目标节点的子节点为止
void splay(int x,int goal)
{
while(tree[x].f != goal){
int fa = tree[x].f , gfa = tree[fa].f;
if(gfa != goal){
((tree[gfa].ch[0] == fa) ^ (tree[fa].ch[0] == x)) ? rotate(x) : rotate(fa);
}
rotate(x);
}
//如果最终目标节点为0,则意味着要把x转到根节点去(因为规定根节点的父节点是0)
if(goal == 0){
root = x;
}
}
void insert(int x)
{
int u = root,fa = 0;
while (u && tree[u].val != x){
fa = u;
u = tree[u].ch[x > tree[u].val];
}
//如果这个点已经存在
if(u){
tree[u].cnt++;
}
else{
u = ++Size;
//如果当前节点不是根节点,就把新建立的节点传给父节点
if( fa ){
tree[fa].ch[x > tree[ fa].val ] = u;
}
tree[u].ch[0] = tree[u].ch[1] = 0;
tree[u].val = x;
tree[u].f = fa;
tree[u].cnt = tree[u].size = 1;
}
//把最新进行操作的节点转到根节点
splay(u,0);
}
void find(int x)
{
//树为空
if( !root ){
return ;
}
int u = root;
while (tree[u].ch[x > tree[u].val] && x != tree[u].val){
u = tree[u].ch[x > tree[u].val];
}
splay(u,0);
}
//求前驱后继(0为前驱,1为后继)
int Presuf(int x,int f)
{
find(x);
//如果没找到该节点返回的是最接近x的值,可能是前驱或者后继,这样就要特殊讨论
if(tree[root].val > x && f){
return root;
}
if(tree[root].val < x && !f){
return root;
}
int u = tree[root].ch[f];
//如果根节点相应子树为空
if( !u ){
return 0;
}
//找前驱点或者后继点
while( tree[u].ch[f^1] ){
u = tree[u].ch[f^1];
}
return u;
}
void Delete(int x)
{
int pre = Presuf( x, 0 ),suf = Presuf( x , 1 );
//将前驱转到根节点,后继转为根节点的子节点,这样要删除的节点就会在
//后继节点的左节点,且为叶子节点
splay( pre , 0);
splay( suf , pre);
int u = tree[suf].ch[0];
if(tree[u].cnt > 1){
//如果有重复的节点,那么就删除一个返回
tree[u].cnt--;
splay( u , 0);
return ;
}
//删除指定的节点
tree[suf].ch[0] = 0;
update(suf);
update(root);//删除后要向上更新
}
//查找第k大
//第k大 > 目标子树的左子树的数量 但是小于目标子树的cnt + 其左子树节点数量
int findkth(int x)
{
if(tree[root].size < x){
return -1;
}
int u = root;
while(1){
//类似二分
//如果x 小于当前节点左子树的数量,就继续在其左子树里找
if(x <= tree[tree[u].ch[0]].size){
u = tree[u].ch[0];
}
else if(x <= tree[tree[u].ch[0]].size + tree[u].cnt){
return u;
}
//从右子树找剩余的
else{
x -= ( tree[tree[u].ch[0]].size + tree[u].cnt );
u = tree[u].ch[1];
}
}
}
int main()
{
//哨兵
insert( INF );
insert( -INF );
scanf("%d",&n);
int op,x;
while(n--){
scanf("%d%d",&op,&x);
if(op == 1){
insert( x );
}
if(op == 2){
Delete( x );
}
if(op == 3){
find( x );
printf("%d\n",tree[tree[root].ch[0]].size);
}
if(op == 4){
int u = findkth( x + 1 );
printf("%d\n",tree[u].val);
}
if(op == 5){
int u = Presuf( x , 0);//前驱
printf("%d\n",tree[u].val);
}
if(op == 6){
int u = Presuf( x , 1);//后继
printf("%d\n",tree[u].val);
}
}
return 0;
}
上面那些是BST基本都有的操作,而一些树的数据结构支持一些特殊的功能,像无旋Treap的可持续化操作
这里的splay树则支持区间翻转的操作
关于区间翻转操作
我们要维护的是序列号,而不是序列中的值。
这里采用了lazy惰性标记,去记录区间是否翻转,类似于线段树
这里浅显的描述一下旋转操作
如果我们要翻转 [ l , r ]区间,那么我们要先找到第 l - 1点,把它旋转到根节点,再把第r + 1点旋转为根节点的右子节点(因为这个点序列号肯定大于根节点的序列号)这样,我们就发现,我们要翻转的区间就为根右子节点的左子树,只需要把这颗左子树的左右儿子翻转一下就可以了
这里我们还要加两个“哨兵”,来进行对于含有1或n区间的翻转操作
洛谷3391
模板
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <string>
#include <cmath>
#include <cstdlib>
using namespace std;
const int MAXN = 1000007;
const int INF = 0x7f7f7f7f;
/********************
splay树
********************/
struct node
{
int f,cnt,val,size,lazy,ch[2];
//f为该节点的父节点(如果f为0,则表示该节点为根节点)
//cnt表示当前该点有多少个重复(包括自己)的
//lazy惰性标记,类似线段树的惰性标记
}tree[MAXN];
int root,Size;
int org[MAXN];//存放序列
//更新节点数
void update(int x)
{
tree[x].size = tree[tree[x].ch[0]].size + tree[tree[x].ch[1]].size + tree[x].cnt;
}
//类似线段树的下推标记操作
void pushdown(int x)
{
if(x && tree[x].lazy){
tree[tree[x].ch[1]].lazy ^= 1;
tree[tree[x].ch[0]].lazy ^= 1;
swap(tree[x].ch[1],tree[x].ch[0]);
tree[x].lazy = 0;
}
}
//旋转
void rotate(int x)
{
int fa = tree[x].f;
int gfa = tree[fa].f;
pushdown(x),pushdown(fa),pushdown(gfa);
//判断x是父节点的哪个儿子,0为左,1为右
int k = (tree[fa].ch[1] == x);
//把x的父节点在gfa的位置用x取代
tree[gfa].ch[tree[gfa].ch[1] == fa] = x;
tree[x].f = gfa;//x的父亲节点变成了gfa
tree[fa].ch[k] = tree[x].ch[k^1];
tree[tree[x].ch[k^1]].f = fa;
tree[x].ch[k^1] = fa;
tree[fa].f = x;
update(fa);
update(x);
}
//splay操作,如果三点一线,且祖父节点不是目标节点,就先转fa再转x,不是三点
//一线,就都转x,一直转到x为目标节点的子节点为止
void splay(int x,int goal)
{
//pushdown(x);
while(tree[x].f != goal){
int fa = tree[x].f , gfa = tree[fa].f;
if(gfa != goal){
((tree[gfa].ch[0] == fa) ^ (tree[fa].ch[0] == x)) ? rotate(x) : rotate(fa);
}
rotate(x);
}
//如果最终目标节点为0,则意味着要把x转到根节点去(因为规定根节点的父节点是0)
if(goal == 0){
root = x;
}
}
//利用递归按照中序进行建树
int build_tree(int l, int r, int fa)
{
if(l > r){
return 0;
}
int mid = (l + r) >> 1;
int now = ++Size;
tree[now].f = fa;
tree[now].val = org[mid];
tree[now].ch[0] = tree[now].ch[1] = 0;
tree[now].cnt = tree[now].size = 1;
tree[now].ch[0] = build_tree(l, mid - 1,now);
tree[now].ch[1] = build_tree(mid + 1,r, now);
update(now);
return now;
}
// void insert(int x)
// {
// int u = root,fa = 0;
// while (u && tree[u].val != x){
// fa = u;
// u = tree[u].ch[x > tree[u].val];
// }
// //如果这个点已经存在
// if(u){
// tree[u].cnt++;
// }
// else{
// u = ++Size;
// //如果当前节点不是根节点,就把新建立的节点传给父节点
// if( fa ){
// tree[fa].ch[x > tree[ fa].val ] = u;
// }
// tree[u].ch[0] = tree[u].ch[1] = 0;
// tree[u].val = x;
// tree[u].f = fa;
// tree[u].cnt = tree[u].size = 1;
// }
// //把最新进行操作的节点转到根节点
// splay(u,0);
// }
// void find(int x)
// {
// //树为空
// if( !root ){
// return ;
// }
// int u = root;
// while (tree[u].ch[x > tree[u].val] && x != tree[u].val){
// u = tree[u].ch[x > tree[u].val];
// }
// splay(u,0);
// }
//求前驱后继(0为前驱,1为后继)
// int Presuf(int x,int f)
// {
// find(x);
// //如果没找到该节点返回的是最接近x的值,可能是前驱或者后继,这样就要特殊讨论
// if(tree[root].val > x && f){
// return root;
// }
// if(tree[root].val < x && !f){
// return root;
// }
// int u = tree[root].ch[f];
// //如果根节点相应子树为空
// if( !u ){
// return 0;
// }
// //找前驱点或者后继点
// while( tree[u].ch[f^1] ){
// u = tree[u].ch[f^1];
// }
// return u;
// }
// void Delete(int x)
// {
// int pre = Presuf( x, 0 ),suf = Presuf( x , 1 );
// //将前驱转到根节点,后继转为根节点的子节点,这样要删除的节点就会在
// //后继节点的左节点,且为叶子节点
// splay( pre , 0);
// splay( suf , pre);
// int u = tree[suf].ch[0];
// if(tree[u].cnt > 1){
// //如果有重复的节点,那么就删除一个返回
// tree[u].cnt--;
// splay( u , 0);
// return ;
// }
// //删除指定的节点
// tree[suf].ch[0] = 0;
// }
//查找第k大
//第k大 > 目标子树的左子树的数量 但是小于目标子树的cnt + 其左子树节点数量
int findkth(int x)
{
if(tree[root].size < x){
return -1;
}
int u = root;
while(1){
//类似二分
//如果x 小于当前节点左子树的数量,就继续在其左子树里找
pushdown(u);
if(x <= tree[tree[u].ch[0]].size){
u = tree[u].ch[0];
}
else if(x <= tree[tree[u].ch[0]].size + tree[u].cnt){
return u;
}
//从右子树找剩余的
else{
x -= ( tree[tree[u].ch[0]].size + tree[u].cnt );
u = tree[u].ch[1];
}
}
}
//区间翻转
void reverse(int x,int y)
{
int l = x - 1,r = y + 1;
l = findkth(l) , r = findkth(r);
//将x - 1转到根节点,y + 1转为根节点的子节点
splay(l,0) , splay(r,l);
//上面旋转完成之后(x , y)区间就位于根节点右子树的左子树
int pos = tree[root].ch[1];
pos = tree[pos].ch[0];
//打上惰性标记
tree[pos].lazy ^= 1;
}
void inorder(int now)
{
pushdown(now);
if(tree[now].ch[0]){
inorder(tree[now].ch[0]);
}
if(tree[now].val != INF && tree[now].val != -INF){
cout<<tree[now].val<<' ';
}
if(tree[now].ch[1]){
inorder(tree[now].ch[1]);
}
}
int main()
{
int n,m,x,y;
cin>>n>>m;
//哨兵
org[1] = -INF , org[n + 2] = INF;
for(int i = 1; i <= n; i++){
org[i + 1] = i;
}
root = build_tree(1 , n + 2, 0);
while(m--){
cin>>x>>y;
reverse(x + 1,y + 1);
}
inorder(root);
return 0;
}
利用splay树做区间提取删除和增加
- 1、提取区间【a, b】其实跟反转的第一步一样,这里不再赘述
- 2、删除区间,就是把提取的区间与根节点的右节点分离,注意更新节点数
- 3、把删除的区间加到某个点 c 后面,第一步先把c转到根节点,再把c + 1转到根的右子树,这样根节点的右子树的左节点为空,直接把提取出的区间加到根节点的右子树的左节点即可
例:hdu 3487
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <string>
#include <cmath>
#include <cstdlib>
using namespace std;
const int MAXN = 3 * 100000 + 7;
const int INF = 0x7f7f7f7f;
/********************
splay树
********************/
struct node
{
int f,cnt,val,size,lazy,ch[2];
//f为该节点的父节点(如果f为0,则表示该节点为根节点)
//cnt表示当前该点有多少个重复(包括自己)的
//lazy惰性标记,类似线段树的惰性标记
}tree[MAXN];
int root,Size;
int org[MAXN];//存放序列
int Count;
//更新节点数
void update(int x)
{
tree[x].size = tree[tree[x].ch[0]].size + tree[tree[x].ch[1]].size + tree[x].cnt;
}
//类似线段树的下推标记操作
void pushdown(int x)
{
if(x && tree[x].lazy){
tree[tree[x].ch[1]].lazy ^= 1;
tree[tree[x].ch[0]].lazy ^= 1;
swap(tree[x].ch[1],tree[x].ch[0]);
tree[x].lazy = 0;
}
}
//旋转
void rotate(int x)
{
int fa = tree[x].f;
int gfa = tree[fa].f;
pushdown(x),pushdown(fa), pushdown(gfa);
//判断x是父节点的哪个儿子,0为左,1为右
int k = (tree[fa].ch[1] == x);
//把x的父节点在gfa的位置用x取代
tree[gfa].ch[tree[gfa].ch[1] == fa] = x;
tree[x].f = gfa;//x的父亲节点变成了gfa
tree[fa].ch[k] = tree[x].ch[k^1];
tree[tree[x].ch[k^1]].f = fa;
tree[x].ch[k^1] = fa;
tree[fa].f = x;
update(fa);
update(x);
}
//splay操作,如果三点一线,且祖父节点不是目标节点,就先转fa再转x,不是三点
//一线,就都转x,一直转到x为目标节点的子节点为止
void splay(int x,int goal)
{
//pushdown(x);
while(tree[x].f != goal){
int fa = tree[x].f , gfa = tree[fa].f;
if(gfa != goal){
((tree[gfa].ch[0] == fa) ^ (tree[fa].ch[0] == x)) ? rotate(x) : rotate(fa);
}
rotate(x);
}
//如果最终目标节点为0,则意味着要把x转到根节点去(因为规定根节点的父节点是0)
if(goal == 0){
root = x;
}
}
//利用递归按照中序进行建树
int build_tree(int l, int r, int fa)
{
if(l > r){
return 0;
}
int mid = (l + r) >> 1;
int now = ++Size;
tree[now].f = fa;
tree[now].val = org[mid];
tree[now].ch[0] = tree[now].ch[1] = 0;
tree[now].cnt = tree[now].size = 1;
tree[now].ch[0] = build_tree(l, mid - 1,now);
tree[now].ch[1] = build_tree(mid + 1,r, now);
update(now);
return now;
}
//查找第k大
//第k大 > 目标子树的左子树的数量 但是小于目标子树的cnt + 其左子树节点数量
int findkth(int x)
{
if(tree[root].size < x){
return -1;
}
int u = root;
while(1){
//类似二分
//如果x 小于当前节点左子树的数量,就继续在其左子树里找
pushdown(u);
if(x <= tree[tree[u].ch[0]].size){
u = tree[u].ch[0];
}
else if(x <= tree[tree[u].ch[0]].size + tree[u].cnt){
return u;
}
//从右子树找剩余的
else{
x -= ( tree[tree[u].ch[0]].size + tree[u].cnt );
u = tree[u].ch[1];
}
}
}
//区间翻转
void reverse(int x,int y)
{
int l = x - 1,r = y + 1;
l = findkth(l) , r = findkth(r);
//将x - 1转到根节点,y + 1转为根节点的子节点
splay( l , 0 ) , splay( r , l );
//上面旋转完成之后(x , y)区间就位于根节点右子树的左子树
int pos = tree[root].ch[1];
pos = tree[pos].ch[0];
//打上惰性标记
tree[pos].lazy ^= 1;
}
void cut(int a,int b,int c )
{
//截取区间 [a, b]
int l = a - 1,r = b + 1;
l = findkth(l) , r = findkth(r);
splay(l,0), splay(r,l);
int u = tree[root].ch[1];
int temp = tree[u].ch[0];
tree[u].ch[0] = 0;
update(u);
update(root);
//将区间【a, b】后接到c后面
l = c - 1, r = c ;
l = findkth(c) , r = findkth(c + 1);
splay(l,0) , splay(r, l);
u = tree[root].ch[1];
tree[u] .ch[0] = temp;
tree[temp].f = u;
update(u);
update(root);
}
void inorder(int now,int n)
{
pushdown(now);
if(tree[now].ch[0]){
inorder(tree[now].ch[0],n);
}
if(tree[now].val != INF && tree[now].val != -INF){
printf("%d",tree[now].val);
if(Count != n){
printf(" ");
Count++;
}
}
if(tree[now].ch[1]){
inorder(tree[now].ch[1],n);
}
}
int main()
{
int n,m;
char s[6];
while(scanf("%d%d",&n,&m) , n >= 0 && m >= 0){
//哨兵
org[1] = -INF , org[n + 2] = INF;
for(int i = 1; i <= n; i++){
org[i + 1] = i;
}
Count = 1;
memset(tree, 0 ,sizeof tree);
root = build_tree(1 , n + 2, 0);
while(m--){
scanf("%s",s);
int a,b,c;
if('C' == s[0]){
scanf("%d%d%d",&a,&b,&c);
cut(a + 1,b + 1,c + 1);
// inorder(root);
// cout<<endl;
}
else{
scanf("%d%d",&a,&b);
reverse(a + 1,b + 1);
// inorder(root);
//cout<<endl;
}
}
inorder(root,n);
printf("\n");
}
return 0;
}