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 必须指向旋转后的根节点。
具体分析旋转步骤
旋转分为两种:左旋和右旋。
右旋将左儿子上移,左旋将右儿子上移。
左右旋并没有本质区别。其目的相同,即将指定节点上移一个位置。
代码实现时左右旋不分开写。
以右旋为例,左旋同理。
设需要上移的节点为 \(now\),其父亲为\(fa\),其祖父为 \(gfa\)。
- 将 \(fa\) 的左儿子指向 \(now\) 的右儿子,且 \(now\) 的右儿子的父亲指向 \(fa\)。
Son[fa][0] = Son[now][1], Fa[Son[now][1]] = fa
- 将 \(now\) 的右儿子指向 \(fa\),且 \(fa\) 的父亲指向 \(now\)。
Son[now][1] = fa, Fa[fa] = now
- 将 \(gfa\) 原来 \(fa\) 所在的儿子位置指向 \(now\),且 \(now\) 的父亲指向 \(gfa\)。
Fa[now] = gfa, Son[fa == Son[gfa][1]] = now
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\)为需要旋转到根的节点) 。
- 若 \(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}\)上的内容进行了复制整理扩充,修改了代码风格让其更加难易于阅读。