Splay

v标题好长!

真是声势浩大,徒有其表。

splay树的基本思路

出于某些原因(cache原理),在访问了某个节点之后,接下来有90%的概率很频繁地再次访问该节点,如果能把这个大概率会被多次访问的结点放到离树根尽可能近的地方,那么就可以节省不少的时间。
(大概如此)

所以要想办法把最近访问的结点扔到距离根节点尽可能近的位置。

著名计算机学家tarjan就想到了办法。

基本的定义

不写这个后文进行不下去啊。

const int MAXN=102030;
struct Splay_Tree
{
	int val,c[2],up;//c[0]代表左儿子,c[1]代表右儿子,up代表父亲
}tree[MAXN];
bool which(int pos)
{
	return tree[tree[pos].up].c[1]==pos;
}//返回pos是它父亲的哪个儿子

splay函数

splay的意思是伸展。
接下来给出的splay函数,能够在保证一直保持着BST的结构的同时,把某个节点伸展到根去。

怎么做呢?
参考AVL树,我们可以一点一点地把这个点旋转上去。

旋转 rotate

在这里插入图片描述

就以上图为例子,假设被旋转点在目标点的左下方。
现在,我们要把红色点转到它的父亲橙色点的上面。

嗯哼哼(试图吸引注意力),我是红点。
rotate的基本思路就是,让我右上方的父亲(因为在右边所以比我大)成为我的右儿子。

我父亲之前在我的哪上方,那就让他去我的哪下方。
然而这样就会有另外三对父子关系收到了威胁。
在这里插入图片描述
也就是说……
在这里插入图片描述
我原来的右儿子(粉色)将何去何从?
我原来的爷爷(蓝色)的儿子(橙色)怎么就没了?
我原来的兄弟(紫色)的父亲(就是我的父亲橙色)怎么就没了?

不要慌张,我们冷静分析。
现在,
需要爸爸的:粉色 紫色 红色
需要儿子的:橙色(一左一右) 蓝色
正好都是三个,看来可以平均分。
在这里插入图片描述

这样就没问题了。

橙色点的右儿子还是它的右儿子(紫色)。
红色点的右儿子(粉色)就接在新的右儿子(橙色)下面,当左儿子。
然后再让红色点接到原来的爷爷(蓝色)下面。

(我写了些什么?)

如果红点在橙色点的右下角那就照照镜子反过来。

也就是说,
如果我的父亲在我的右上方,也就是我是我父亲的左儿子。
那么就把我的父亲拉下来成为我的新的右儿子。
此时,我的父亲的左儿子就不是我了,我的右儿子的位置被挤占了(没了和父亲(我)的连接),我爷爷的儿子也没有了。
于是让我原来的右儿子成为我的原来的父亲(现在新的右儿子)的左儿子,然后我篡权夺位,成为我原来爷爷的新儿子。

因此rotate函数可以这么写:

void rotate(int pos)
{
	int up=tree[pos].up,upup=tree[up].up,is=which(pos);
	
	//如果我是我父亲的左儿子(is=0)的话,就让我的右儿子当我父亲的新的左儿子,我父亲成为我的右儿子
	tree[up].c[is]=tree[pos].c[!is],tree[tree[pos].c[!is]].up=up;
	tree[pos].c[!is]=up,tree[up].up=pos;//爸爸认儿子的同时要记得儿子认爸爸啊
	
	//我的新爸爸就是我原来的爷爷,我原来的爷爷的新的儿子就是我
	tree[pos].up=upup;
	if(upup) tree[upup].c[tree[upup].c[1]==up]=pos;
	//当然如果爷爷是虚空(原来的爸爸就是根节点)的话,就不能爸爸认儿子了
	//还有还有,因为父子关系发生了说不清道不明的改变,所以这里不好用which,要用which一开始定义的时候的用
}

伸展 splay

我们发现通过rotate我们能在不改变树的平衡性的同时让某个点上升一层,但是这离我们的目标(旋转到根节点)还差得远。
在这里插入图片描述
所以就有了splay操作:让某个结点通过一次又一次rotate转到根节点。比方说:

逆流而上的你眼前或许有无数曲折崎岖道路,也许离终点遥遥无期,但是,
结点 到达根源 叶子结点

人一定要有梦想,没有梦想和咸鱼有什么区别?!
人一定要有梦想,没有梦想和咸鱼有什么区别?!
人一定要有梦想,没有梦想和咸鱼有什么区别?!

咳咳,话说回来,逆流而上的你眼前或许有无数曲折崎岖道路,也许离终点遥遥无期,但是,绝无无法行走的路 (定义如此,走不了就不叫路了) ,只要你想要到达,就没有无法克服的障碍,只要你想办法的话。
在你一步一步往上rotate的时候,你的道路大概可以分为以下三类,六种:
在这里插入图片描述

还有一类没有画上去,就是爸爸就是根节点,没有爷爷的情况,这个直接一个rotate就解决了。

折线形没有什么好说的,一步一步rotate上去吧。
关于直线型:就是我是爸爸的a儿子,我爸爸是我爷爷的a儿子的情况。
科学家们告诉我们,这个时候应该先rotate(爸爸),再rotate(我)。
如果仍然是一直埋头苦干rotate(我),这样的自平衡方法叫做Spaly;而先rotate(父亲),再rotate(我)的自平衡方法叫做splay。
如下图:
在这里插入图片描述
乍一看差不多,甚至某道题目(BZOJ1036树的统计Count)我把splay换成了spaly会快一些(5600ms->5000ms),但是咨询了Freopen/Kyle/wk大佬后,Freopen/Kyle/wk告诉我,“可以证明splay更优,而且出数据的时候可以卡spaly。”
(所以说科学家等于wk?)

哇。

总结一下:情况 应对方法

我爸爸是根,我没有爷爷 rotate(我)
我,我爸爸,我爷爷呈一条直线 rotate(父),rotate(我)
我,我爸爸,我爷爷呈一条折线 rotate(我),rotate(我)

所以就可以得到splay函数:

void splay(int pos)
{
	while(tree[pos].up)
	{
		if(tree[tree[pos].up].up && which(tree[pos].up)==which(pos)) rotate(tree[pos].up);
		rotate(pos);
	}
	root=pos;//一个全局变量root来记录splay树的根
}

当然,用splay操作可以使一个节点上升到它上面的任意一个结点

插入 insert

和正常的二叉平衡树一样,先找到对应的位置,直接插入,没问题的。
然后再spaly到根节点上去。

void insert(int pos,int val,int up) //调用的时候就用insert(root,某个值,0)
{
	if(!pos)
	{
		tree[++n].val=val,tree[n].up=up;
		tree[up].c[tree[n].val>tree[up].val]=n;//认别人做爸爸的同时,别人也要认你做儿子
		splay(pos);
		//此时不同的题有不同的操作
		return ;
	}
	insert(tree[pos].c[val>tree[pos].val],val,pos);
	//这里默认每个点的值都不同,如果相同的话就在不同的题里面有不同的处理方式
}

前驱&后继 pre&nxt

也有叫upper和lower的,还有等等名字。
和一般的二叉平衡树没有什么区别。

//默认了各个数不相同,不过知道了原理之后想怎么样都无所谓吧
int pre(int val) 
{
	int pos=root,ans=-2147483647;
	while(pos)
	{
		if(tree[pos].val<val) ans=max(ans,tree[pos].val);
		pos=tree[pos].c[val>tree[pos].val];
	}
	return ans;
}
//所谓前驱就是小于某个值的最大值,也可以说是这个点的左子树里面最靠右的那个端点。
	
int nxt(int val)
{
	int pos=root,ans=2147483647;
	while(pos)
	{
		if(tree[pos].val>val) ans=min(ans,tree[pos].val);
		pos=tree[pos].c[val>tree[pos].val];
	}
	return ans;
}
//基本上是复制之前写AVL的时候写的那个

求数的排名和排名上的数

我称之为:getrank() getrank()getrank()和rankget() rankget()rankget()。

如果采用惰性方法的话就很方便。
现在我们给每个节点新增两个变量,cnt和siz。
cnt代表这个点上重复有多少个数。比方说val=1的点的cnt=4,就代表插入了4个1,都被塞到同一个点里。
siz代表子树里数的个数(也就是说,不是子树点的个数,而是子树里各个点的cnt的值的和)
这样子的话,插入和删除会有些许变化。记得在树的结构或者点的cnt改变的时候pushup一下来维护siz。(也就是insert和delete和splay的时候→也就是splay的时候)到时候给出普通平衡树模板的时候一并看吧。

然后rankget,就是和找前驱差不多了,用找前驱的方法加上siz这个变量就可以轻松把前驱是谁转换成求前驱有几个的问题了。就是找到前驱然后把前驱的siz+1就是答案了。

然后是getrank,感觉像find和getrank的结合体,有了siz和cnt,我们就知道每个pos的val所对应的排名的区间是多少了。就是[tree[tree[pos].c[0]].siz+1,tree[tree[pos].c[0]].siz+tree[pos].cnt]
{left: [tree[tree[pos].c[0]].siz+1,right: tree[tree[pos].c[0]].siz+tree[pos].cnt]}[tree[tree[pos].c[0]].siz+1,tree[tree[pos].c[0]].siz+tree[pos].cnt],
如果在这个范围里,那么就找到了,如果给定的排名在这个排名区间的左边,那就说明我们要找的数比当前的数要小,那么就向左二分下去,如果在右边就向右边。
需要稍微注意一下的是,向右边搜的时候,给定的排名要减去**(tree[tree[pos].c[0]].siz+tree[pos].cnt) (tree[tree[pos].c[0]].siz+tree[pos].cnt)(tree[tree[pos].c[0]].siz+tree[pos].cnt),**
因为当前节点的siz是它的左儿子+右儿子+自己,比方说左儿子和右儿子是[1,2,3,4,5,6,7]和[8,9,10]找第八名,当然应该在右儿子里面找了,但是右儿子只有3个数没有第8名,所以把8-7(左儿子的siz)得到1,我们应该在右儿子里面查询第1大的数。

void pushup(int pos)
{
	tree[pos].siz=tree[tree[pos].c[0]].siz+tree[tree[pos].c[1]]+tree[pos].cnt;
}
//目前的pushup只有siz一个,因为查找前驱后继不会改变数的形态更不会改变siz所以不pushup
//调用的时候调用getrank(val,root),对了,还有可能找不到,所以记得特判
int getrank(int val,int pos)
{
	if(pos==0) return 2147483647;
	if(val==tree[pos].val) return tree[tree[pos].c[0]].siz+1;
	else  
	{
		if(val<tree[pos].val) return getrank(val,tree[pos].c[0]);
		else return getrank(val,tree[pos].c[1])+tree[tree[pos].c[0]].siz+tree[pos].cnt;
	}
}
int rankget(int rak,int pos)
{
	if(pos==0) return -2147483647;
	if(rak>=tree[tree[pos].c[0]].siz+1 && rak<=tree[tree[pos].c[0]].siz+tree[pos].cnt) return tree[pos].val;
	else
	{
		if(rak<tree[tree[pos].c[0]].siz+1)  return rankget(rak,tree[pos].c[0]);
		return rankget(rak-tree[tree[pos].c[0]].siz-tree[pos].cnt,tree[pos].c[1]);
	}
}

删除 deleted

因为delete这个函数名已经有了,所以加了一个d。

采用惰性删除。

现在要分好几种情况来讨论。

首先,如果cnt-1之后还有剩余,就平安无事什么也不用干,cnt–就是了。

如果cnt==1,也就是删除了这个点就没有了:
(说实话空留一个cnt=0的点在那里浪费时间空间好像没什么问题啊)
先把这个要删除的东西splay到根节点处

如果要删除的点(目前已经splay到根了),没有儿子: 这棵树的最后的一个数被你删了,这棵树完了。
如果只有一个儿子:那么就直接把这个根节点移除掉,并把根节点的位置传给那个儿子。
在这里插入图片描述
如果有两个儿子的话:
把前驱找到并splay上来,然后把被删除点的右儿子接到前驱的右边,自己消失掉。
在这里插入图片描述

void deleted(int val)
{
	getrank(val);//随便整一下把目标点splay上来
	if(--tree[root].cnt) return;//如果删掉一个还有剩余,就无事发生
	if(!tree[root].c[0] && !tree[root].con[1]) root=0;//如果连根无子无孙,那这棵树就没了
	else if(!tree[root].c[1]) root=tree[root].c[0],tree[root].up=0,pushUp(root);
	else if(!tree[root].c[0]) root=tree[root].c[1],tree[root].up=0,pushUp(root);
	//如果只有一个儿子,那就让那个儿子接替自己的位置
	else 
	{
		int pre=tree[root].c[0],pos=root;
		while(tree[pre].c[1]) pre=tree[pre].c[1];
		//找前驱
		splay(pre),tree[tree[pos].c[1]].up=root,tree[root].c[1]=tree[pos].c[1],pushup(root);
		//把前驱再转上来,现在目标点(pos)就是根节点的右儿子
	}
}

现在普通平衡树的各个功能就写好了。
然后是,

合并 join

有一颗Splay树(记为S1)的所有节点都比另一颗Splay树(记为S2)的最小的节点小的时候
在这里插入图片描述
于是让S1最大的节点Splay到S1的根,然后把S2接到S1的右下方。

好鸡肋的功能。
图来自杨思雨的论文。

分离 split

给定数x,把一颗splay树分成两棵树,其中一棵树的值都小于x,另一颗都大于x。
首先把x这个点splay到根,然后它的左子树和右子树即为所求。

求最值 min&max

这个就一直往左or右走就是了。

翻转 turn

现在来考虑做文艺平衡树。
文艺平衡树要我们支持对一个数列进行区间翻转再输出。

首先,为了把用一棵树来存一个数列,所以和普通的SBT不同(普通的SBT的中序遍历是一个不下降序列)的,现在我们维护的splay树的中序遍历是这个区间本身。也就是从按权值不下降排序到下标不下降排序。

举个例子就是一个数组{1,3,4,5,6,7,2,4,5,2},在一个普通的Splay树中,它的中序遍历是{1,2,2,3,4,4,5,5,6,7},在支持区间翻转的Splay树中,中序遍历是**{1,3,4,5,6,7,2,4,5,2}**。

然后怎么区间翻转呢?

先建一颗树,按照题目所要求的,就假设N=12,那么数列就是{1,2,3,4,5,6,7,8,9,10,11,12},建成splay树以后可以长这个样子:
在这里插入图片描述

上图的确是跑splay的时候跑出来的。

现在我们可以看到,中序遍历就是{1,2,3,4,5,6,7,8,9,10,11,12},假设现在我们要翻转区间[l,r],比方说[4,6],就是图中绿点的区间:
在这里插入图片描述
我们先想办法把这个区间给独立出来
那么我们先把r+1这个点Splay到根节点,也就是Splay(7)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
转上来了。

然后再把l-1转到r+1的左儿子,也就是Splay(3,tree[root].c[0])。
毕竟上文说了,可以把一个点splay到它上方任意一个节点,而它肯定在根节点的左侧,那么根节点的左儿子一定在它的上方。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
现在我们就把要操作的区间独立出来了,就是根的左儿子的右子树。(是一颗树)
那么现在就可以做很多事情了。

比方说翻转,对于这个独立出来的子树,要翻转相当于交换每个节点的左右儿子,但是来如果要交换的话,那么就会很麻烦,况且一个区间被多次翻转之后,很有可能翻转回来,就浪费很多时间空间。

所以打懒标记吧。
标记一下这个点是否要翻转左右儿子,输出的时候如果有标记就翻转地输出。
然后每次翻转区间的时候不需要对整个区间打标记,只需要在最上面的那个点那里打标记就行了。
如果要访问这个区间里没有打过标记的点,那么必然会访问刚才打过标记的那个“最上面那个点”,那么在访问那个点的时候就把标记下传给儿子们,接下来访问某个儿子,访问这个儿子的时候再下传给它的儿子……直到我们访问到要找的那个点,此时它已经得到懒标记了,而整个过程几乎没有浪费时间在给暂时无关的结点打标记上。

代码就丢在文艺平衡树里面吧。

对了对了,因为要访问l−1 和r+1 这两个结点,所以为了不在翻转区间[1,x]或[x,n] ]的时候爆掉,要在1号点之前加一个-inf,在n号点之后加一个inf,既然这样那么哪个点对应哪个值就一定要想清楚了。

其他区间操作(以SuperMemo为例)

Your friend, Jackson is invited to a TV show called SuperMemo in which the participant is told to play a memorizing game. At first, the host tells the participant a sequence of numbers, A1,A2,…An {A1, A2, … An}A1,A2,…An. Then the host performs a series of operations and queries on the sequence which consists:

ADD x y D ADD x y ~DADD x y D: Add D to each number in sub-sequence Ax…Ay {Ax … Ay}Ax…Ay For example, performing “ADD 2 4 1 ADD ~2 ~4 ~1ADD 2 4 1” on 1,2,3,4,5 {1, 2, 3, 4, 5}1,2,3,4,5results in 1,3,4,5,5 {1, 3, 4, 5, 5}1,3,4,5,5
REVERSE x y REVERSE ~x ~yREVERSE x y: reverse the sub-sequence Ax…Ay {Ax … Ay}Ax…Ay. For example, performing “REVERSE 2 4 REVERSE ~2 ~4REVERSE 2 4” on 1,2,3,4,5 {1, 2, 3, 4, 5}1,2,3,4,5 results in 1,4,3,2,5 {1, 4, 3, 2, 5}1,4,3,2,5
REVOLVE x y T REVOLVE ~x ~y ~TREVOLVE x y T: rotate sub-sequence Ax…Ay {Ax … Ay}Ax…Ay T times. For example, performing “REVOLVE 2 4 2 REVOLVE 2 4 ~2REVOLVE 2 4 2” on 1,2,3,4,5 {1, 2, 3, 4, 5}1,2,3,4,5 results in 1,3,4,2,5 {1, 3, 4, 2, 5}1,3,4,2,5
INSERT x P INSERT~ x ~PINSERT x P: insert P after Ax. For example, performing “INSERT 2 4 INSERT~ 2~ 4INSERT 2 4” on 1,2,3,4,5 {1, 2, 3, 4, 5}1,2,3,4,5 results in 1,2,4,3,4,5 {1, 2, 4, 3, 4, 5}1,2,4,3,4,5
DELETE x DELETE ~xDELETE x: delete Ax. For example, performing “DELETE 2 DELETE ~2DELETE 2” on 1,2,3,4,5 {1, 2, 3, 4, 5}1,2,3,4,5 results in 1,3,4,5 {1, 3, 4, 5}1,3,4,5
MIN x y MIN x yMIN x y: query the participant what is the minimum number in sub-sequence Ax…Ay {Ax … Ay}Ax…Ay. For example, the correct answer to “MIN2 4 MIN 2 ~4MIN2 4” on 1,2,3,4,5 {1, 2, 3, 4, 5}1,2,3,4,5 is 2 22
To make the show more interesting, the participant is granted a chance to turn to someone else that means when Jackson feels difficult in answering a query he may call you for help. You task is to watch the TV show and write a program giving the correct answer to each query in order to assist Jackson whenever he calls.

翻译

写一个数据结构支持六种操作:
①ADD x y D ,对于区间[x,y]每个数都加上D DD。
②REVERSE x y翻转区间[x,y]。
③REVOLVE x y T这个厉害了,把区间[x,y]里的每个数在这个区间里面循环右移T次,举个例子就是:1,2,3,4,5→5,1,2,3,4→4,5,1,2,3→3,4,5,1,2
④INSERT x P ,在x 点的后面插入一个值为P的点。
⑤DELETE x DELETE ~xDELETE x,删掉点x xx。
⑥MIN x y,求区间[x,y] 的最小值。

一个个来

对于ADD操作,先把这个区间独立出来,然后打一个加法懒标记。
对于REVERSE操作,上面有。
对于REVOLVE操作,声势浩大,徒有其表,首先先把T%=(y-x+1);,那么就是把这个区间的后T个数移到前面y-x+1-T个数的前面;那么就是把前y-x+1-T个数REVERSE,把后T个数REVERSE,然后再把整个区间REVERSE就行了。
在这里插入图片描述
(这个字体的6写得像4)
对于INSERT操作,因为它要求在某个点后面插入值,所以先把这个值x当成一个区间[x,x] (数学考试这么写是会被扣分的)把它独立出来,也就是先把x+1 x+1x+1 S p l a y Splay Splay 到根节点,再把x−1 S p l a y Splay Splay 到根节点的儿子,那么x xx就在x−1的右儿子,然后再把P接上去就是了。
对于DELETE操作,和INSERT一样,把x独立出来以后直接取下来就是了。
对于MIN操作,因为我们的树不是按照数值大小关系来排序的,所以要额外开一个值来记录子树里的最小值,和siz一起push_up。

普通平衡树

别人的

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
#include<climits>
typedef long long LL;
using namespace std;
int RD(){
    int out = 0,flag = 1;char c = getchar();
    while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
    while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
    return flag * out;
    }
//第一次打treap,不压行写注释XD
const int maxn = 1000019,INF = 1e9;
//平衡树,利用BST性质查询和修改,利用随机和堆优先级来保持平衡,把树的深度控制在log N,保证了操作效率
//基本平衡树有以下几个比较重要的函数:新建,插入,删除,旋转
//节点的基本属性有val(值),dat(随机出来的优先级)
//通过增加属性,结合BST的性质可以达到一些效果,如size(子树大小,查询排名),cnt(每个节点包含的副本数)等
int na;
int ch[maxn][2];//[i][0]代表i左儿子,[i][1]代表i右儿子
int val[maxn],dat[maxn];
int size[maxn],cnt[maxn];
int tot,root;
int New(int v){//新增节点,
    val[++tot] = v;//节点赋值
    dat[tot] = rand();//随机优先级
    size[tot] = 1;//目前是新建叶子节点,所以子树大小为1
    cnt[tot] = 1;//新建节点同理副本数为1
    return tot;
    }
void pushup(int id){//和线段树的pushup更新一样
    size[id] = size[ch[id][0]] + size[ch[id][1]] + cnt[id];//本节点子树大小 = 左儿子子树大小 + 右儿子子树大小 + 本节点副本数
    }
void build(){
    root = New(-INF),ch[root][1] = New(INF);//先加入正无穷和负无穷,便于之后操作(貌似不加也行)
    pushup(root);//因为INF > -INF,所以是右子树,
    }
void Rotate(int &id,int d){//id是引用传递,d(irection)为旋转方向,0为左旋,1为右旋
    int temp = ch[id][d ^ 1];//旋转理解:找个动图看一看就好(或参见其他OIer的blog)
    ch[id][d ^ 1] = ch[temp][d];//这里讲一个记忆技巧,这些数据都是被记录后马上修改
    ch[temp][d] = id;//所以像“Z”一样
    id = temp;//比如这个id,在上一行才被记录过,ch[temp][d]、ch[id][d ^ 1]也是一样的
    pushup(ch[id][d]),pushup(id);//旋转以后size会改变,看图就会发现只更新自己和转上来的点,pushup一下,注意先子节点再父节点
    }//旋转实质是({在满足BST的性质的基础上比较优先级}通过交换本节点和其某个叶子节点)把链叉开成二叉形状(从而控制深度),可以看图理解一下
void insert(int &id,int v){//id依然是引用,在新建节点时可以体现
    if(!id){
        id = New(v);//若节点为空,则新建一个节点
        return ;
        }
    if(v == val[id])cnt[id]++;//若节点已存在,则副本数++;
    else{//要满足BST性质,小于插到左边,大于插到右边
        int d = v < val[id] ? 0 : 1;//这个d是方向的意思,按照BST的性质,小于本节点则向左,大于向右
        insert(ch[id][d],v);//递归实现
        if(dat[id] < dat[ch[id][d]])Rotate(id,d ^ 1);//(参考一下图)与左节点交换右旋,与右节点交换左旋
        }
    pushup(id);//现在更新一下本节点的信息
    }
void Remove(int &id,int v){//最难de部分了
    if(!id)return ;//到这了发现查不到这个节点,该点不存在,直接返回
    if(v == val[id]){//检索到了这个值
        if(cnt[id] > 1){cnt[id]--,pushup(id);return ;}//若副本不止一个,减去一个就好
        if(ch[id][0] || ch[id][1]){//发现只有一个值,且有儿子节点,我们只能把值旋转到底部删除
            if(!ch[id][1] || dat[ch[id][0]] > dat[ch[id][1]]){//当前点被移走之后,会有一个新的点补上来(左儿子或右儿子),按照优先级,优先级大的补上来
                Rotate(id,1),Remove(ch[id][1],v);//我们会发现,右旋是与左儿子交换,当前点变成右节点;左旋则是与右儿子交换,当前点变为左节点
                }
            else Rotate(id,0),Remove(ch[id][0],v);
            pushup(id);
            }
        else id = 0;//发现本节点是叶子节点,直接删除
        return ;//这个return对应的是检索到值de所有情况
        }
    v < val[id] ? Remove(ch[id][0],v) : Remove(ch[id][1],v);//继续BST性质
    pushup(id);
    }
int get_rank(int id,int v){
    if(!id)return 0;//若查询值不存在,返回
    if(v == val[id])return size[ch[id][0]] + 1;//查询到该值,由BST性质可知:该点左边值都比该点的值(查询值)小,故rank为左儿子大小 + 1
    else if(v < val[id])return get_rank(ch[id][0],v);//发现需查询的点在该点左边,往左边递归查询
    else return size[ch[id][0]] + cnt[id] + get_rank(ch[id][1],v);//若查询值大于该点值。说明询问点在当前点的右侧,且此点的值都小于查询值,所以要加上cnt[id]
    }
int get_val(int id,int rank){
    if(!id)return INF;//一直向右找找不到,说明是正无穷
    if(rank <= size[ch[id][0]])return get_val(ch[id][0],rank);//左边排名已经大于rank了,说明rank对应的值在左儿子那里
        else if(rank <= size[ch[id][0]] + cnt[id])return val[id];//上一步排除了在左区间的情况,若是rank在左与中(目前节点)中,则直接返回目前节点(中区间)的值
    else return get_val(ch[id][1],rank - size[ch[id][0]] - cnt[id]);//剩下只能在右区间找了,rank减去左区间大小和中区间,继续递归
    }
int get_pre(int v){
    int id = root,pre;//递归不好返回,以循环求解
    while(id){//查到节点不存在为止
        if(val[id] < v)pre = val[id],id = ch[id][1];//满足当前节点比目标小,往当前节点的右侧寻找最优值
        else id = ch[id][0];//无论是比目标节点大还是等于目标节点,都不满足前驱条件,应往更小处靠近
        }
    return pre;
    }
int get_next(int v){
    int id = root,next;
    while(id){
        if(val[id] > v)next = val[id],id = ch[id][0];//同理,满足条件向左寻找更小解(也就是最优解)
        else id = ch[id][1];//与上方同理
        }
    return next;
    }
int main(){
    build();//不要忘记初始化[运行build()会连同root一并初始化,所以很重要]
    na = RD();
    for(int i = 1;i <= na;i++){
        int cmd = RD(),x = RD();
        if(cmd == 1)insert(root,x);//函数都写好了,注意:需要递归的函数都从根开始,不需要递归的函数直接查询
        else if(cmd == 2)Remove(root,x);
        else if(cmd == 3)printf("%d\n",get_rank(root,x) - 1);//注意:因为初始化时插入了INF和-INF,所以查询排名时要减1(-INF不是第一小,是“第零小”)
            else if(cmd == 4)printf("%d\n",get_val(root,x + 1));//同理,用排名查询值得时候要查x + 1名,因为第一名(其实不是)是-INF
        else if(cmd == 5)printf("%d\n",get_pre(x));
        else if(cmd == 6)printf("%d\n",get_next(x));
        }
    return 0;
    }

文艺平衡树

别人的yyb

题解

这里的Splay维护的显然不再是权值排序
现在按照的是序列中的编号排序(不过在这道题目里面就是权值诶。。。)
那么,继续考虑,其实最终的结果也就是整颗Splay的中序遍历(平衡树的性质诶)
那么,现在如果按照权值来维护显然是不正确的
继续找找规律,发现,如果一个点在序列中的位置为第K个
那么,他就是平衡树的第K大(就当做普通的Splay来看的话)
所以,序列中的位置就变成了区间的第K大点
继续考虑如何翻转
翻转也就是整颗子树的每一个节点的左右儿子交换
因此,只要在根节点的地方打一个标记
在旋转之前下方一下标记就行了
最后输出的时候输出的就是Splay的中序遍历
至于初始的Splay怎么建立,可以直接构造完美的Splay
像我这种比较懒得,直接弄了一个insert。。。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
#define MAX 200000
inline int read()
{
    int x=0,t=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return x*t;
}
struct Node
{
    int ch[2];
    int ff,v;
    int size;
    int mark;
    void init(int x,int fa)
        {
            ff=ch[0]=ch[1]=0;
            size=1;v=x;ff=fa;
        }
}t[MAX];
int N,root,M,tot;
inline void pushup(int x)
{
    t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+1;
}
inline void pushdown(int x)
{
    if(t[x].mark)
    {
        t[t[x].ch[0]].mark^=1;
        t[t[x].ch[1]].mark^=1;
        t[x].mark=0;
        swap(t[x].ch[0],t[x].ch[1]);
    }
}
inline void rotate(int x)
{
    int y=t[x].ff;
    int z=t[y].ff;
    int k=t[y].ch[1]==x;
    t[z].ch[t[z].ch[1]==y]=x;
    t[x].ff=z;
    t[y].ch[k]=t[x].ch[k^1];
    t[t[x].ch[k^1]].ff=y;
    t[x].ch[k^1]=y;
    t[y].ff=x;
    pushup(y);pushup(x);
}
inline void Splay(int x,int goal)
{
    while(t[x].ff!=goal)
    {
        int y=t[x].ff;int z=t[y].ff;
        if(z!=goal)
            (t[z].ch[1]==y)^(t[y].ch[1]==x)?rotate(x):rotate(y);
        rotate(x);
    }
    if(goal==0)root=x;
}
inline void insert(int x)
{
    int u=root,ff=0;
    while(u)ff=u,u=t[u].ch[x>t[u].v];
    u=++tot;
    if(ff)t[ff].ch[x>t[ff].v]=u;
    t[u].init(x,ff);
    Splay(u,0);
}
inline int Kth(int k)
{
    int u=root;
    while(233)
    {
        pushdown(u);
        if(t[t[u].ch[0]].size>=k)u=t[u].ch[0];
        else if(t[t[u].ch[0]].size+1==k)return u;
        else k-=t[t[u].ch[0]].size+1,u=t[u].ch[1];
    }
}
void write(int u)
{
    pushdown(u);
    if(t[u].ch[0])write(t[u].ch[0]);
    if(t[u].v>1&&t[u].v<N+2)printf("%d ",t[u].v-1);
    if(t[u].ch[1])write(t[u].ch[1]);
}
inline void Work(int l,int r)
{
    l=Kth(l);
    r=Kth(r+2);
    Splay(l,0);
    Splay(r,l);
    t[t[t[root].ch[1]].ch[0]].mark^=1;
}
int main()
{
    N=read();M=read();
    for(int i=1;i<=N+2;++i)insert(i);
    while(M--)
    {
        int l=read(),r=read();
        Work(l,r);
    }
    write(root);
    printf("\n");
    return 0;
}

SuperMemo
咕咕咕

Splay的优缺点

相较于AVL和Treap,Splay可以少存一个平衡因子。
Splay还有一个很重要的特性,那就是不稳定性,可能飚的很快,也可能被神秘卡掉。
所以“在严谨场合”不建议使用。
代码实现比AVL要简单一些。

参考文章
以下每一篇都比我的这个好:
https://blog.csdn.net/a_comme_amour/article/details/79382104
https://www.cnblogs.com/cjyyb/p/7499020.html
https://blog.csdn.net/chenxiaoran666/article/details/81414567
http://www.cnblogs.com/dalt/p/8167168.html(时间复杂度分析)
https://blog.csdn.net/CABI_ZGX/article/details/82819882 (SuperMemo)
https://blog.csdn.net/DERITt/article/details/50485008 (更多的区间操作)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值