splay树 模板

越学越把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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值