[平衡树]伸展树(Splay)

前言

本来老师以为我们学过Splay,今天讲LCT,结果我们没学过,于是… …
听了会儿课,有点迷,还是自学写篇博客8。
才开始学习Splay,可能有些瑕疵,望指出。

Splay

什么是Splay

	假设要对一个二叉搜索树执行一系列查找操作,为了使得总时间最小,那么被查找频率高的节点自然就要放在靠近
根的位置。于是想到一个简单的设计方案,在每次查找之后对树进行重构,把被查找的条目搬到离树根近一点的位置。
顺着这个思路,Splay诞生了。
	Splay是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列旋转把该节点搬移到
树根,同时使得该条路径上的点尽量靠近树根。

为什么要Splay

	对于线段树或树状数组,维护的下标区间都是不变的,利用这个性质,可以对其二分建立树。但是在支持破坏元素
顺序的操作后,必定维护的下标区间会发生改变,间接导致建立树的形态不同,因此在原序列为最底层的基础上,每
层二分建树的方法是不可行的。基础数据结构都只支持不改变元素顺序的操作。也就是说,这些数据结构都不支持插
入或删除元素,或是破坏元素顺序的操作。
	如果我们需要维护序列最值与区间和,并要求在线段树的基础操作外支持插入、删除、翻转区间、分裂及合并的操
作,该怎么办呢?那么就需要用Splay了。

思路

我们知道,在一些情况下,二叉搜索树的时间复杂度会由 O ( log ⁡ n ) O(\log n) O(logn)变为 O ( n ) O(n) O(n)(也就是树形退化为单链)。因此,我们要尽量维护二叉查找树的平衡,使树的期望高度为 log ⁡ n \log n logn
而维护这样的平衡的树据结构,我们就叫做平衡树。Splay就是其中的一种。

Splay最重要的操作是伸展操作(splaying)。伸展操作就是将一个节点向上逐步旋转(Rotate)到指定节点(一般为根节点),不改变其二叉搜索树的性质。将高频搜索节点尽量靠近根节点,就能够减少搜索的访问次数。

单旋

对于节点 x x x ,如果 x x x 点的父亲 p p p 点就为根节点的话,那么 x x x 向上转一次就到根了。
如果 x x x p p p 的左儿子则右旋(Zig),反之左旋(Zag)。

给出一个右旋的例子(图片来源:知乎):
Zig
x x x 点转到根,把 x x x 和左子树保留, x x x 的右子树剥离出来,把 p p p 点插入 x x x 的右子树,再把 x x x 的右子树插到 p p p 的左子树。

双旋

但是,这样的单旋是很可能会T的。如果把一个很小的值转上去,那么这个点的左子树会很小,右子树会很大。在这样的极端情况下,Splay的形态会退化成链,时间复杂度又是 O ( n ) O(n) O(n)的了,就失去了平衡树的意义。这个时候,我们就要分情况讨论,进行双旋

情况一:

对于点 x x x , x x x 的父节点 p p p , p p p 的父节点 q q q ,若 x x x p p p p p p q q q的对应左右关系相同,即三点为一条直线。则先旋转 p p p,再旋转 x x x(为什么不是先 x x x p p p 呢?是为了之后引入势进行复杂度分析。可以发现,这样的旋转是可逆的)。那么,这样的双旋就对应了 Zig-ZigZag-Zag

举个栗子:
Zig-Zig

情况二:

对于点 x x x , x x x 的父节点 p p p , p p p 的父节点 q q q ,若 x x x p p p p p p q q q的对应左右关系相同,即三点为一条折线。那么就将 x x x 旋转两次,对应为 Zig-ZagZag-Zig

举个栗子:
在这里插入图片描述

维护信息

Splay在伸展操作后不改变中序遍历序,即其对应的序列。维护信息自底向上维护,每个结点除了维护该结点对应序列元素自身的信息外,还需要维护子树对应序列上区间的信息并。每次伸展操作开始前下传标记,每次旋转操作结束后更新结点信息(类似于线段树)。

最基本的信息是子树大小(size,子树结点个数,子树大小用于确定序列第 k k k 位位置)、左右子结点编号以及父结点编号。还有一些根据题意维护的变量。

实现

建树

采用线段树的建树方法。时间复杂度为 O ( n ) O(n) O(n),树高为 log ⁡ n \log n logn

Code:
咕咕咕
k k k号元素位置

很好想,就不说了

Code:
LL GetKth(LL x)
	{
		LL Now = Root;
		while ( 1 )
		{
			if (Tree[Now].Son[0] && x <= Tree[Tree[Now].Son[0]].Size)
				Now = Tree[Now].Son[0];
			else if (x > Tree[Tree[Now].Son[0]].Size + Tree[Now].Cnt)
			{
				x -= (Tree[Tree[Now].Son[0]].Size + Tree[Now].Cnt);
				Now = Tree[Now].Son[1];
			}
			else return Now;
		}
	}
插入与删除(Insert & Delete)

单点插入(在 k k k号元素后插入新元素):
将第 k k k号点右子树插入该点,再将该点插入 k k k号点即可。

Code:
void Insert(LL x)
	{
		LL Now = Root, Pos = 0;
		while (Now && Tree[Now].Val != x)
		{
			Pos = Now;
			Now = Tree[Now].Son[x > Tree[Now].Val];
		}
		if ( Now )
			Tree[Now].Cnt ++;
		else
		{
			Now = ++ tot;
			if ( Pos )
				Tree[Pos].Son[x > Tree[Pos].Val] = Now;
			Tree[Now].Son[0] = Tree[Now].Son[1] = 0;
			Tree[Now].Fa = Pos;
			Tree[Now].Val = x;
			Tree[Now].Size = Tree[Now].Cnt = 1;
		}
		Splay(Now, 0); // 将它旋到根 
	}
	void Delete(LL x)
	{
		LL Over = GetPre( x ), Next = GetBack( x );
		Splay(Over, 0);
		Splay(Next, Over);
		LL Del = Tree[Next].Son[0];
		if (Tree[Del].Cnt > 1)
		{
			Tree[Del].Cnt --;
			Splay(Del, 0);
		}
		else Tree[Next].Son[0] = 0;
	}

区间插入(插入区间):
对于区间插入,利用伸展的性质。先将插入位置左侧元素伸展至根,再将插入位置的右侧元素伸展至根的右子结点。完成这样的伸展后,根结点的右结点即为要插入的点,在它的左子树插入即可。

Code:
咕咕咕
分裂与合并(与Treap类似)

利用伸展操作,我们可以实现快速分裂合并。

  • 对于分裂操作,如果我们需要在第 k k k 个结点右侧将序列分成两段,我们只需要将第 k k k 个结点旋转到根结点,并断开其与右子树的连接。得到的两棵新树对应着分成的两段序列。
  • 对于合并操作,我们同样只需要将前一段序列对应树的最右侧结点旋转至根,并将后一段序列对应的树作为根结点的右子树即可。
  • Code:
咕咕咕
区间信息维护

每个结点除了自身对应序列元素的信息,还需要维护的是,以该结点为根结点的子树对应的区间的信息并。在伸展操作前后的更新信息与标记下传,都保证了信息的准确。查询信息,只需要像区间插入一样,将子序列对应的子树旋转至根结点的右子结点的左子树,并对其进行操作即可。添加标记或修改信息亦同理。以子树大小为例,每次更新信息时,重置该结点的子树大小为1,并累加其左右子结点的子树大小。

Code:
咕咕咕
区间翻转

添加一个翻转的Lazy,在下放时交换左右结点即可。

Code:
咕咕咕
区间最值、区间求和、区间加法与区间赋值

区间最值只需要维护每个结点其对应的区间的最值。查询时按照区间信息维护中提及的方法旋转并查询即可。区间求和亦同理。区间加法与区间赋值亦同理,但请注意新标记对当前结点其他信息的影响。与线段树的维护类似。

Code:
咕咕咕

整合一下:

洛谷板 Splay 普通平衡树
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define MAXN 100005
#define LL long long
#define Int register int
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
inline void read(LL &x)
{
	x = 0;
	LL f = 1;
	char s = getchar();
	while (s < '0' || s > '9')
	{
		if (s == '-')
			f = -1;
		s = getchar();
	}
	while (s >= '0' && s <= '9')
	{
		x = (x << 3) + (x << 1) + (s ^ 48);
		s = getchar();
	}
	x *= f;
}
struct SPlayer
{
	LL Root, tot;
	struct SplayTree
	{
		LL Fa, Son[2], Cnt, Val, Size, Maxx;
	}Tree[MAXN << 2];
	bool ZY(LL x)
	{
		return (Tree[Tree[x].Fa].Son[1] == x);
	}
	void Update(LL x)
	{
		Tree[x].Size = Tree[x].Cnt + Tree[Tree[x].Son[0]].Size + Tree[Tree[x].Son[1]].Size;
	}
	void Rotate(LL x) // 将 x 旋转到 y 
	{
		LL y = Tree[x].Fa;
		LL z = Tree[y].Fa;
		bool Fx = ZY( x );
		LL w = Tree[x].Son[Fx ^ 1];
		Tree[y].Son[Fx] = w;
		Tree[w].Fa = y;
		Tree[z].Son[ZY(y)] = x;
		Tree[x].Fa = z;
		Tree[x].Son[Fx ^ 1] = y;
		Tree[y].Fa = x;
		Update( y );
		Update( x );
	}
	void Splay(LL x,LL Goal) // 将 x 旋到 Gola
	{
		while (Tree[x].Fa != Goal)
		{
			LL y = Tree[x].Fa;
			LL z = Tree[y].Fa;
			if (z != Goal) // 双旋 
			{
				if (ZY(x) == ZY( y )) // 直线 
					Rotate( y );
				else Rotate( x ); // 折线 
			}
			Rotate( x );
		}
		if (! Goal)
			Root = x; 
	}
	void Insert(LL x)
	{
		LL Now = Root, Pos = 0;
		while (Now && Tree[Now].Val != x)
		{
			Pos = Now;
			Now = Tree[Now].Son[x > Tree[Now].Val];
		}
		if ( Now )
			Tree[Now].Cnt ++;
		else
		{
			Now = ++ tot;
			if ( Pos )
				Tree[Pos].Son[x > Tree[Pos].Val] = Now;
			Tree[Now].Son[0] = Tree[Now].Son[1] = 0;
			Tree[Now].Fa = Pos;
			Tree[Now].Val = x;
			Tree[Now].Size = Tree[Now].Cnt = 1;
		}
		Splay(Now, 0); // 将它旋到根 
	}
	void Find(LL x) // 找到 x值的位置,将它旋到根 
	{
		LL Now = Root;
		while (Tree[Now].Son[x > Tree[Now].Val] && x != Tree[Now].Val)
			Now = Tree[Now].Son[x > Tree[Now].Val];
		Splay(Now, 0); 
	}
	LL GetKth(LL x)
	{
		LL Now = Root;
		while ( 1 )
		{
			if (Tree[Now].Son[0] && x <= Tree[Tree[Now].Son[0]].Size)
				Now = Tree[Now].Son[0];
			else if (x > Tree[Tree[Now].Son[0]].Size + Tree[Now].Cnt)
			{
				x -= (Tree[Tree[Now].Son[0]].Size + Tree[Now].Cnt);
				Now = Tree[Now].Son[1];
			}
			else return Now;
		}
	}
	LL GetPre(LL x)
	{
		Find( x );
		if (Tree[Root].Val < x)
			return Root;
		LL Now = Tree[Root].Son[0];
		while (Tree[Now].Son[1])
			Now = Tree[Now].Son[1];
		return Now;
	}
	LL GetBack(LL x)
	{
		Find( x );
		if (Tree[Root].Val > x)
			return Root;
		LL Now = Tree[Root].Son[1];
		while (Tree[Now].Son[0])
			Now = Tree[Now].Son[0];
		return Now;
	}
	void Delete(LL x)
	{
		LL Over = GetPre( x ), Next = GetBack( x );
		Splay(Over, 0);
		Splay(Next, Over);
		LL Del = Tree[Next].Son[0];
		if (Tree[Del].Cnt > 1)
		{
			Tree[Del].Cnt --;
			Splay(Del, 0);
		}
		else Tree[Next].Son[0] = 0;
	}
}_;
int main()
{
	LL n;
	read( n );
	_.Insert( INF );
	_.Insert( - INF );
	for (Int i = 1; i <= n; ++ i)
	{
		LL Or, x;
		read( Or ); read( x );
		switch ( Or )
		{
			case 1:
			{
				_.Insert( x );
				break;
			}
			case 2:
			{
				_.Delete( x );
				break;
			}
			case 3:
			{
				_.Find( x );
				printf("%lld\n", _.Tree[_.Tree[_.Root].Son[0]].Size);
				break;
			}
			case 4:
			{
				printf("%lld\n", _.Tree[_.GetKth(x + 1)].Val);
				break;
			}
			case 5:
			{
				printf("%lld\n", _.Tree[_.GetPre( x )].Val);
				break;
			}
			case 6:
			{
				printf("%lld\n", _.Tree[_.GetBack( x )].Val);
				break;
			}
		}
	}
	return 0;
}

复杂度分析

咕咕咕

例题

  1. POJ 3580 SuperMemo
题解:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define MAXN 100005
#define LL int
//#define LL long long
#define Int register int
using namespace std;
inline void read(LL &x)
{
	x = 0;
	LL f = 1;
	char s = getchar();
	while (s < '0' || s > '9')
	{
		if (s == '-')
			f = -1;
		s = getchar();
	}
	while (s >= '0' && s <= '9')
	{
		x = (x << 3) + (x << 1) + (s ^ 48);
		s = getchar();
	}
	x *= f;
}
inline LL Max(LL x,LL y)
{
	return x > y ? x : y;
}
inline LL Min(LL x,LL y)
{
	return x < y ? x : y;
}
inline void Swap(LL &x,LL &y)
{
	LL temp = x;
	x = y;
	y = temp;
}
LL n, a[MAXN];
struct Splayer
{
	LL Root, tot;
	struct SplayTree
	{
		bool Lazy_Reverse;
		LL Fa, Son[2], Lazy_Add, Size, Maxx, Minn, Val;
	}Tree[MAXN << 1];
	inline void NewNode(LL Zhi)
	{
		tot ++;
		Tree[tot].Size = 1;
		Tree[tot].Val = Tree[tot].Maxx = Tree[tot].Minn = Zhi;
		//printf("%lld %lld %lld\n", tot, Tree[tot].Val, Tree[tot].Minn);
		Tree[tot].Son[0] = Tree[tot].Son[1] = Tree[tot].Fa = Tree[tot].Lazy_Add = Tree[tot].Lazy_Reverse = 0;
	}
	inline void Update(LL x)
	{
		Tree[x].Size = Tree[Tree[x].Son[0]].Size + Tree[Tree[x].Son[1]].Size + 1;
		Tree[x].Maxx = Tree[x].Minn = Tree[x].Val;
		if (Tree[x].Son[0])
			Tree[x].Maxx = Max(Tree[x].Maxx, Tree[Tree[x].Son[0]].Maxx),
			Tree[x].Minn = Min(Tree[x].Minn, Tree[Tree[x].Son[0]].Minn);
		if (Tree[x].Son[1])
			Tree[x].Maxx = Max(Tree[x].Maxx, Tree[Tree[x].Son[1]].Maxx),
			Tree[x].Minn = Min(Tree[x].Minn, Tree[Tree[x].Son[1]].Minn);
	}
	void PushDown(LL x)
	{
		if (Tree[x].Lazy_Add)
		{
			if (Tree[x].Son[0])
			{
				Tree[Tree[x].Son[0]].Val += Tree[x].Lazy_Add;
				Tree[Tree[x].Son[0]].Lazy_Add += Tree[x].Lazy_Add;
				Tree[Tree[x].Son[0]].Maxx += Tree[x].Lazy_Add;
				Tree[Tree[x].Son[0]].Minn += Tree[x].Lazy_Add;
			}
			if (Tree[x].Son[1])
			{
				Tree[Tree[x].Son[1]].Val += Tree[x].Lazy_Add;
				Tree[Tree[x].Son[1]].Lazy_Add += Tree[x].Lazy_Add;
				Tree[Tree[x].Son[1]].Maxx += Tree[x].Lazy_Add;
				Tree[Tree[x].Son[1]].Minn += Tree[x].Lazy_Add;
			}
			Tree[x].Lazy_Add = 0;
		}
		if (Tree[x].Lazy_Reverse)
		{
			if (Tree[x].Son[0])
				Tree[Tree[x].Son[0]].Lazy_Reverse ^= 1;
			if (Tree[x].Son[1])
				Tree[Tree[x].Son[1]].Lazy_Reverse ^= 1;
			Swap(Tree[x].Son[0], Tree[x].Son[1]);
			Tree[x].Lazy_Reverse = 0;
		}
	}
	void Build(LL x,LL Pre)
	{
		if (x == n + 2)
			return ;
		NewNode( a[x] );
		if ( Pre )
		{
			Tree[tot].Fa = Pre;
			Tree[Pre].Son[1] = tot;
		}
		else Root = tot;
		LL Now = tot;
		Build(x + 1, tot);
		Update( Now );
	}
	bool ZY(LL x)
	{
		return (Tree[Tree[x].Fa].Son[1] == x);
	}
	inline void Rotate(LL x)
	{
		LL y = Tree[x].Fa;
		LL z = Tree[y].Fa;
		bool Fx = ZY( x );
		LL B = Tree[x].Son[Fx ^ 1];
		Tree[y].Son[Fx] = B;
		Tree[B].Fa = y;
		Tree[z].Son[ZY( y )] = x;
		Tree[x].Fa = z;
		Tree[x].Son[Fx ^ 1] = y;
		Tree[y].Fa = x;
		Update( y );
		Update( x );
	}
	inline void Splay(LL x,LL Goal)
	{
		while (Tree[x].Fa != Goal)
		{
			LL y = Tree[x].Fa;
			LL z = Tree[y].Fa;
			if (z != Goal)
			{
				if (ZY( x ) == ZY( y ))
					Rotate( y );
				else Rotate( x );
			}
			Rotate( x );
		}
		if (! Goal)
			Root = x;
	}
	inline void ToPlace(LL Kth,LL Pos)
	{
		LL Now = Root;
		while (1 + 1 == 2)
		{
			PushDown( Now );
			LL Len = Tree[Tree[Now].Son[0]].Size + 1;
			if (Kth == Len)
				break ;
			if (Kth < Len)
				Now = Tree[Now].Son[0];
			if (Kth > Len)
				Kth -= Len, Now = Tree[Now].Son[1];
		}
		Splay(Now, Pos);
	}
	inline void Add_QJ(LL l,LL r,LL Zhi)
	{
		ToPlace(l, 0);
		ToPlace(r + 2, Root);
		LL Cha = Tree[Tree[Root].Son[1]].Son[0];
		Tree[Cha].Lazy_Add += Zhi;
		Tree[Cha].Maxx += Zhi;
		Tree[Cha].Minn += Zhi;
		Tree[Cha].Val += Zhi;
	}
	inline void Insert(LL Pos,LL Zhi)
	{
		ToPlace(Pos + 1, 0);
		ToPlace(Pos + 2, Root);
		NewNode( Zhi );
		Tree[Tree[Root].Son[1]].Son[0] = tot;
		Tree[tot].Fa = Tree[Root].Son[1];
	}
	inline void Delete(LL Pos)
	{
		ToPlace(Pos, 0);
		ToPlace(Pos + 2, Root);
		Tree[Tree[Root].Son[1]].Son[0] = 0;
	}
	inline LL GetMax(LL l,LL r)
	{
		ToPlace(l, 0);
		ToPlace(r + 2, Root);
		return Tree[Tree[Tree[Root].Son[1]].Son[0]].Maxx;
	}
	inline LL GetMin(LL l,LL r)
	{
		ToPlace(l, 0);
		ToPlace(r + 2, Root);
		return Tree[Tree[Tree[Root].Son[1]].Son[0]].Minn;
	}
	inline void Reverse(LL l,LL r)
	{
		if (l == r)
			return ;
		ToPlace(l, 0);
		ToPlace(r + 2, Root);
		Tree[Tree[Tree[Root].Son[1]].Son[0]].Lazy_Reverse ^= 1;
	}
	inline void Revolve(LL l,LL r,LL Wei)
	{
		LL Len = r - l + 1;
		LL Fen = (Wei % Len + Len) % Len;
		if ( Fen )
		{
			ToPlace(r - Fen + 1, 0);
			ToPlace(r + 2, Root);
			LL Zhuan = Tree[Tree[Root].Son[1]].Son[0];
			Tree[Tree[Root].Son[1]].Son[0] = 0;
			ToPlace(l, 0);
			ToPlace(l + 1, Root);
			Tree[Tree[Root].Son[1]].Son[0] = Zhuan;
			Tree[Zhuan].Fa = Tree[Root].Son[1];
		}
	}
}Fuck_;
int main()
{
	read( n );
	for (Int i = 1; i <= n; ++ i)
		read( a[i] );
	Fuck_.Build(0, 0);
	LL m;
	read( m );
	for (Int i = 1; i <= m; ++ i)
	{
		char Or[8];
		scanf("%s", Or);
		switch ( Or[0] )
		{
			case 'A':
			{
				LL l, r, x;
				read( l ); read( r ); read( x );
				Fuck_.Add_QJ(l, r, x);
				break;
			}
			case 'R':
			{
				switch ( Or[3] )
				{
					case 'E':
					{
						LL l, r;
						read( l ); read( r );
						Fuck_.Reverse(l, r);
						break;
					}
					case 'O':
					{
						LL l, r, x;
						read( l ); read( r ); read( x );
						Fuck_.Revolve(l, r, x);
						break;
					}
				}
				break;
			}
			case 'I':
			{
				LL x, Zhi;
				read( x ); read( Zhi );
				Fuck_.Insert(x, Zhi);
				break;
			}
			case 'D':
			{
				LL x;
				read( x );
				Fuck_.Delete( x );
				break;
			}
			case 'M':
			{
				LL l, r;
				read( l ); read( r );
				LL Ans = Fuck_.GetMin(l, r);
				printf("%d\n", Ans);
				break;
			}
		}
	}
	return 0;
}
  1. bzoj 1251 序列终结者
题解:
咕咕咕
  1. bzoj 1500 维修数列
题解:
咕咕咕
  1. HYSBZ 1269 文本编辑器editor
题解:
咕咕咕
  1. NOI 2004 郁闷的出纳员
题解:
咕咕咕
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值