Splay ver1.0


前置知识 :二叉搜索树

写在前面

本文为 luckyblock 的早期屑作,学习 splay 请移步:「笔记」Splay

简介

一种二叉搜索树的实现方法。
通过不断将某节点旋转到根节点,使得整棵树仍满足二叉搜索树的性质。
且保持平衡不至于退化为链,以保证复杂度。

一些定义

节点维护信息

Root NodeNum Fa[x] Son[x][0/1] Val[x] Cnt[x] Size[x]
根节点编号 节点个数 节点父亲 左/右儿子编号 节点权值 权值出现次数 子树大小

宏定义

#define Fat (t[now].Fa)
#define ls (t[now].Son[0])
#define rs (t[now].Son[1])

操作

基本操作

  • \(\text{Pushup}(now)\) 维护节点Size。
  • \(\text{GetSonNum}(now)\) 判断节点 \(now\) 是其父的左儿子/右儿子。
  • \(\text{NodeClear}(now)\) 销毁节点\(now\)
void NodeClear(int now) {ls = rs = Fat = t[now].Size = t[now].Cnt = t[now].Val = 0;}
bool GetSonNum(int now) {return t[Fat].Son[1] == now;}
void Pushup(int now) 
{
	if(now)
	{
	  t[now].Size = t[now].Cnt;
	  if(ls) t[now].Size += t[ls].Size;
	  if(rs) t[now].Size += t[rs].Size;
	}
}

旋转操作

\(\text{Rotate}(now)\) 将节点 \(now\) 上移一个位置。

旋转需要保证

  • 整棵树的中序遍历不变(不破坏二叉搜索树的性质)。
  • 节点维护的信息依然正确有效。
  • Root 必须指向旋转后的根节点。

具体分析旋转步骤

旋转分为两种:左旋和右旋。

右旋将左儿子上移,左旋将右儿子上移。
左右旋并没有本质区别。其目的相同,即将指定节点上移一个位置。
代码实现时左右旋不分开写。

Rotate

以右旋为例,左旋同理。
设需要上移的节点为 \(now\),其父亲为\(fa\),其祖父为 \(gfa\)

graph1

  1. \(fa\) 的左儿子指向 \(now\) 的右儿子,且 \(now\) 的右儿子的父亲指向 \(fa\)
    Son[fa][0] = Son[now][1], Fa[Son[now][1]] = fa

graph2

  1. \(now\) 的右儿子指向 \(fa\),且 \(fa\) 的父亲指向 \(now\)
    Son[now][1] = fa, Fa[fa] = now

graph3

  1. \(gfa\) 原来 \(fa\) 所在的儿子位置指向 \(now\),且 \(now\) 的父亲指向 \(gfa\)
    Fa[now] = gfa, Son[fa == Son[gfa][1]] = now

graph4

void Rotate(int now)
{
	int fa = Fat, gfa = t[fa].Fa, WhichSon = GetSonNum(now);
	t[fa].Son[WhichSon] = t[now].Son[WhichSon ^ 1];
	t[t[fa].Son[WhichSon]].Fa = fa;
	t[now].Son[WhichSon ^ 1] = fa;
	t[fa].Fa = now, t[now].Fa = gfa;
	if(gfa) t[gfa].Son[t[gfa].Son[1] == fa] = now;
	Pushup(fa), Pushup(now);
}

Splay操作

\(\text{Splay}(now)\)\(now\) \(\text{Rotate}\) 至根的位置。

使用Splay实现时规定:
每访问一个节点后,都要强制将其旋转到根节点。 此时旋转操作具体分6种情况讨论(\(x\)为需要旋转到根的节点) 。

splay

  • \(x\) 的父亲为根节点,直接将 \(x\) 左旋或右旋(图 1,2)。
  • \(x\) 的父亲非根节点,且 \(x\) 和父亲的儿子类型相同。 先将其父亲左旋或右旋,然后将 \(x\) 右旋或左旋(图 3,4)。
  • \(x\) 的父亲非根节点,且 \(x\) 和父亲的儿子类型不同。将 \(x\) 左旋再右旋、或者右旋再左旋(图 5,6)。

代码实现较简单,建议手动模拟以理解。

void Splay(int now)
{
	for(int fa = Fat; (fa = Fat) != 0; Rotate(now))
	  if(t[fa].Fa) Rotate((GetSonNum(now) == GetSonNum(fa)) ? fa : now);
	Root = now;
}

插入操作

\(\text{Insert}(val)\) 新建一个权值为\(val\)的节点,将其插入树中。

设插入的值为 \(val\)

  • 若树空,则直接插入根并退出。
  • 按照二叉查找树的性质向下找:
    • 若当前节点的权值等于 \(val\) 则增加当前节点的大小,并更新信息。
    • 否则一直向下,找到空节点并插入。

最后将当前节点进行 Splay 操作。

void Insert(int val) 
{
	if(! Root)
	{
	  ++ NodeNum;
	  t[NodeNum].Son[0] = t[NodeNum].Son[1] = t[NodeNum].Fa = 0;
	  Root = NodeNum;
	  t[NodeNum].Size = t[NodeNum].Cnt ++; //
	  t[NodeNum].Val = val;
	  return ;
	}
	int now = Root, fa = 0;
	while(1)
	{
	  if(val == t[now].Val) 
	  {
	  	t[now].Cnt ++; Pushup(now), Pushup(fa);
		Splay(now); break;
	  }
	  fa = now, now = t[now].Son[t[now].Val < val];
	  if(! now)
	  {
	  	++ NodeNum;
		t[NodeNum].Son[0] = t[NodeNum].Son[1] = 0;
		t[NodeNum].Fa = fa; t[NodeNum].Val = val;
		t[NodeNum].Size = t[NodeNum].Cnt = 1;
		t[fa].Son[t[fa].Val < val] = NodeNum;
		Pushup(fa), Pushup(NodeNum); 
		Splay(NodeNum); break;
	  }
	}
}

查询给定权值的排名

\(\text{QueryRank}(val)\) 查询权值 \(val\) 的排名。

  • \(val\) 比当前节点的权值小,向其左子树查找。
  • 如果 \(val\) 比当前节点的权值大,将答案加上左子树(Size)和当前节点(Cnt)的大小,向其右子树查找。
  • 如果 \(val\) 与当前节点的权值相同,将答案加 1 并返回。
  • 若找不到与 \(val\) 权值相等的节点,则返回 -1作为答案。

最后进行Splay操作。

int QueryRank(int val)
{
	int now = Root, ret = 0;
	while(1)
	{
	  if(! now) return - 1;
	  if(val < t[now].Val) now = ls;
	  else
	  {
	  	ret += (ls ? t[ls].Size : 0);
	  	if(val == t[now].Val) {Splay(now); return ret + 1;}
	  	ret += t[now].Cnt; now = rs;
	  }
	}
}

查询给定排名的权值

\(\text{Kth}(rank)\) 查询排名\(rank\)的权值。

根据二叉树搜索树左小右大的性质 和 维护的子树大小可得答案。

  • 若左子树非空 且 剩余排名 \(rank\) 不大于左子树的 Size,则向左子树查找。
  • 否则将 \(rank\) 减去左子树的和 当前根的大小。
    如果此时\(rank\)的值小于等于 0,则返回当前根节点的权值,否则继续向右子树查找。
  • 若找不到排名为 \(rank\) 的节点,则返回 -1作为答案。
int Kth(int rank)
{
	int now = Root;
	while(1)
	{
	  if(! now) return - 1;
	  if(ls && rank <= t[ls].Size) now = ls;
	  else
	  {
	  	int tmp = (ls ? t[ls].Size : 0) + t[now].Cnt;
	  	if(rank <= tmp) return t[now].Val;
	  	rank -= tmp; now = rs;
	  }
	}
}

查询前驱后继

\(\text{QueryPrecursor}()\) 查询的前驱。

前驱定义为小于 \(val\) 的最大的数。
利用Splay的性质,那么查询前驱可以转化为:
\(val\) 插入(会被Splay至根),前驱即为 \(val\) 的左子树中最右边的节点。最后将\(val\)删除即可。

int QueryPrecursor()
{
	int now = t[Root].Son[0];
	while(rs) now = rs;
	return now;
}

Insert(x), printf("%d\n", t[QueryPrecursor()].Val), Delete(x);

\(\text{QuerySuccessor}()\) 查询后继。

后继定义为大于 \(val\) 的最小的数,查询方法和前驱类似,即为 \(val\) 的右子树中最左边的节点。

int QuerySuccessor()
{
	int now = t[Root].Son[1];
	while(ls) now = ls;
	return now;
}

Insert(x), printf("%d\n", t[QuerySuccessor()].Val), Delete(x);

删除操作

\(\text{Delete}(val)\) 删除一个权值为 \(val\)的节点。

先将 \(val\) 旋转到根的位置,之后分类讨论。

  • 若有不止一个 \(val\),那么该节点Cnt - 1并退出。

  • \(val\) 没有儿子节点,那么直接将当前节点\(\text{NodeClear}\) 并退出。

  • 若 只有一个儿子,那么先将当前节点\(\text{NodeClear}\),再把唯一儿子作为根节点。

  • 否则将 \(val\) 的前驱旋转到根并作为根节点,将\(val\)的右子树接到根节点的右子树上,最后更新根的信息。

void Delete(int val)
{
	int ret = QueryRank(val);
	if(ret == - 1) return ;
	if(t[Root].Cnt > 1) {t[Root].Cnt --; Pushup(Root); return ;}
	if(! t[Root].Son[0] && ! t[Root].Son[1]) {NodeClear(Root); Root = 0; return ;}
	if(! t[Root].Son[0])
	{
	  int OldRoot = Root;
	  Root = t[Root].Son[1], t[Root].Fa = 0;
	  NodeClear(OldRoot); return ;
	}
	if(! t[Root].Son[1])
	{
	  int OldRoot = Root;
	  Root = t[Root].Son[0], t[Root].Fa = 0;
	  NodeClear(OldRoot); return ;
	}
	int LeftMax = QueryPrecursor(), OldRoot = Root;
	Splay(LeftMax);
	t[Root].Son[1] = t[OldRoot].Son[1], t[t[OldRoot].Son[1]].Fa = Root;
	NodeClear(OldRoot); Pushup(Root);
}

完整代码

//Splay
/*
By:Luckyblock
Typical Splay
*/
#include <cstdio> 
#include <algorithm>
#include <ctype.h>
#define Fat (t[now].Fa)
#define ls (t[now].Son[0])
#define rs (t[now].Son[1])
#define ll long long
const int MARX = 1e6 + 10;
//=============================================================
struct SpalyNode
{
	int Fa, Son[2];
	int Val, Size, Cnt;
} t[MARX];
int N, NodeNum, Root;
//=============================================================
inline int read()
{
    int s = 1, w = 0; char ch = getchar();
    for(; !isdigit(ch); ch = getchar()) if(ch == '-') s = -1;
    for(; isdigit(ch); ch = getchar()) w = (w << 1) + (w << 3) + (ch ^ '0');
    return s * w;
}
void NodeClear(int now) {ls = rs = Fat = t[now].Size = t[now].Cnt = t[now].Val = 0;}
bool GetSonNum(int now) {return t[Fat].Son[1] == now;}
void Pushup(int now) 
{
	if(now)
	{
	  t[now].Size = t[now].Cnt;
	  if(ls) t[now].Size += t[ls].Size;
	  if(rs) t[now].Size += t[rs].Size;
	}
}
void Rotate(int now)
{
	int fa = Fat, gfa = t[fa].Fa, WhichSon = GetSonNum(now);
	t[fa].Son[WhichSon] = t[now].Son[WhichSon ^ 1];
	t[t[fa].Son[WhichSon]].Fa = fa;
	t[now].Son[WhichSon ^ 1] = fa;
	t[fa].Fa = now, t[now].Fa = gfa;
	if(gfa) t[gfa].Son[t[gfa].Son[1] == fa] = now;
	Pushup(fa), Pushup(now);
}
void Splay(int now)
{
	for(int fa = Fat; (fa = Fat) != 0; Rotate(now))
	  if(t[fa].Fa) Rotate((GetSonNum(now) == GetSonNum(fa)) ? fa : now);
	Root = now;
}
void Insert(int val) 
{
	if(! Root)
	{
	  ++ NodeNum;
	  t[NodeNum].Son[0] = t[NodeNum].Son[1] = t[NodeNum].Fa = 0;
	  Root = NodeNum;
	  t[NodeNum].Size = t[NodeNum].Cnt ++; //
	  t[NodeNum].Val = val;
	  return ;
	}
	int now = Root, fa = 0;
	while(1)
	{
	  if(val == t[now].Val) 
	  {
	  	t[now].Cnt ++; Pushup(now), Pushup(fa);
		Splay(now); break;
	  }
	  fa = now, now = t[now].Son[t[now].Val < val];
	  if(! now)
	  {
	  	++ NodeNum;
		t[NodeNum].Son[0] = t[NodeNum].Son[1] = 0;
		t[NodeNum].Fa = fa; t[NodeNum].Val = val;
		t[NodeNum].Size = t[NodeNum].Cnt = 1;
		t[fa].Son[t[fa].Val < val] = NodeNum;
		Pushup(fa), Pushup(NodeNum); 
		Splay(NodeNum); break;
	  }
	}
}
int QueryRank(int val)
{
	int now = Root, ret = 0;
	while(1)
	{
	  if(! now) return - 1;
	  if(val < t[now].Val) now = ls;
	  else
	  {
	  	ret += (ls ? t[ls].Size : 0);
	  	if(val == t[now].Val) {Splay(now); return ret + 1;}
	  	ret += t[now].Cnt; now = rs;
	  }
	}
}
int QueryVal(int rank)
{
	int now = Root;
	while(1)
	{
	  if(! now) return - 1;
	  if(ls && rank <= t[ls].Size) now = ls;
	  else
	  {
	  	int tmp = (ls ? t[ls].Size : 0) + t[now].Cnt;
	  	if(rank <= tmp) return t[now].Val;
	  	rank -= tmp; now = rs;
	  }
	}
}
int QueryPrecursor()
{
	int now = t[Root].Son[0];
	while(rs) now = rs;
	return now;
}
int QuerySuccessor()
{
	int now = t[Root].Son[1];
	while(ls) now = ls;
	return now;
}
void Delete(int val) //
{
	int ret = QueryRank(val);
	if(ret == - 1) return ;
	if(t[Root].Cnt > 1) {t[Root].Cnt --; Pushup(Root); return ;}
	if(! t[Root].Son[0] && ! t[Root].Son[1]) {NodeClear(Root); Root = 0; return ;}
	if(! t[Root].Son[0])
	{
	  int OldRoot = Root;
	  Root = t[Root].Son[1], t[Root].Fa = 0;
	  NodeClear(OldRoot); return ;
	}
	if(! t[Root].Son[1])
	{
	  int OldRoot = Root;
	  Root = t[Root].Son[0], t[Root].Fa = 0;
	  NodeClear(OldRoot); return ;
	}
	int LeftMax = QueryPrecursor(), OldRoot = Root;
	Splay(LeftMax);
	t[Root].Son[1] = t[OldRoot].Son[1], t[t[OldRoot].Son[1]].Fa = Root;
	NodeClear(OldRoot); Pushup(Root);
}
//=============================================================
int main()
{
	N = read();
	for(int i = 1; i <= N; i ++)
	{
	  int opt = read(),x = read();
	  if(opt == 1) Insert(x);
	  if(opt == 2) Delete(x);
	  if(opt == 3) printf("%d\n", QueryRank(x));
	  if(opt == 4) printf("%d\n", QueryVal(x));
	  if(opt == 5) Insert(x), printf("%d\n", t[QueryPrecursor()].Val), Delete(x);
	  if(opt == 6) Insert(x), printf("%d\n", t[QuerySuccessor()].Val), Delete(x);
	}
	return 0;
}

复杂度证明

建议百度。


写在最后

参考资料
\(\text{Oi-Wiki-Splay}\)

\(\text{Oi-Wiki}\)上的内容进行了复制整理扩充,修改了代码风格让其更加易于阅读。

posted @ 2020-04-28 11:27  Luckyblock  阅读(296)  评论(2编辑  收藏  举报