『笔记』Splay(一)
写在前面
\(SPLAY\) ??
SPLAY
定义
百度者云:
伸展树$ (Splay Tree)$ ,也叫分裂树,是一种二叉排序树,它能在 \(O(log n)\) 内完成插入、查找和删除操作。
在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠*树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根*一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。
它的优势在于不需要记录用于*衡树的冗余信息。
啥玩意?
学长者言:
\(Splay\) 是一种二叉搜索树的维护方式。它通过不断将某节点旋转到根节点,使得整棵树仍满足二叉搜索树的性质,且保持*衡不至于退化为链,以保证复杂度。
思想
对于查找频率较高的节点,使其处于离根节点相对较*的节点
那什么是所谓查找频率较高的节点?
我们可以将每次操作的节点看作是查找频率相对较高的节点,注意这里是相对。
那么如何实现把该节点转移到根的操作呢?
可以对树进行旋转操作。
旋转
功能
用于将指定的节点上移一个位置。
实现
旋转操作分为左旋和右旋两种,顾名思义就是向左旋转和向右旋转,手玩模拟就是顺时针和逆时针旋转。
二者的实现思路和目的皆相同,只不过是指定节点为左孩子时使用右旋,右孩子时使用左旋。
by Luckyblock
在进行旋转操作时,应遵循一下原则:
-
整棵树的中序遍历不变,即不破坏二叉搜索树树的基本性质
-
节点维护的信息依然正确有效
-
\(root\) 必须指向旋转后的根节点
其实类似于 Treap 的旋转操作,只是多了对 fa 数组的维护。
当 x 是 y 的左孩子时
我们如果让 \(x\) 成为 y 的父亲,只会影响到三个点的关系,即 \(r\) 与 \(x\) , \(x\) 与 \(y\) , \(x\) 与 \(r\) 。
根据二叉树的性质,
\(r\) 会成为 \(y\) 的左儿子;
\(y\) 会成为 \(x\) 的右儿子;
\(x\) 会成为 \(r\) 的儿子,左右依据 \(y\) 是 \(r\) 的什么儿子;
然后就变成这么个样:
当 x 是 y 的右孩子时
转化过程与上面的一样:
代码
void zig(int now) //右旋
{
int fa_ = fa[now];
int z = fa[fa_];
lc[fa_] = rc[now];
fa[rc[now]] = fa_;
rc[now] = fa_;
fa[fa_] = now, fa[now] = z;
if (fa_ == lc[z])
lc[z] = now;
else if (fa_ == rc[z])
rc[z] = now;
update(fa_);
update(now);
if (rt == fa_)
rt = now;
}
void zag(int now) //左旋
{
int fa_ = fa[now];
int z = fa[fa_];
rc[fa_] = lc[now];
fa[lc[now]] = fa_;
lc[now] = fa_;
fa[fa_] = now, fa[now] = z;
if (fa_ == lc[z])
lc[z] = now;
else if (fa_ == rc[z])
rc[z] = now;
update(fa_);
update(now);
if (rt == fa_)
rt = now;
}
写代码的过程中我们发现,第二种情况就是吧第一种情况的 \(x\) , \(y\) 调换了位置,那我们能不能将张两种情况合并起来实现呢?
既然要实现上述功能,就必须明确当前是他的父节点的左孩子还是右孩子
int check_son(int now_)//获得儿子类型
{
return now_ == son[fa[now_]][1];
}
/*
如果是
左孩子返回 true
右孩子返回 false
*/
那么单旋函数就可以写出来了:
void rotate(int now_)
{
int fa_ = fa[now_];
bool chson = check_son(now_);
if (fa[fa[now_]])
son[fa[fa[now_]]][check_son(fa[now_])] = now_; //更新祖父
fa[now_] = fa[fa[now_]];
son[fa_][chson] = son[now_][chson ^ 1]; //更新 fa 的儿子
fa[son[fa_][chson]] = fa_;
son[now_][chson ^ 1] = fa_; //上提 now_
fa[fa_] = now_;
push_up(fa_);
push_up(now_);
}
伸展操作
定义
\(Splay\) 利用伸展操作趋**衡。
也就是说 \(Splay\) 实现将但钱访问过的节点旋转至根节点的位置。
功能
这样可以使经常访问的节点访问的速度优化得非常快(因为节点几乎完全在根节点附*),而且在旋转的过程中整棵树趋*于*衡。
实现
两种方式,单旋和双旋。
最简单的办法,对于 \(x\) 这个节点,不断地上提该节点,直到它到达 \(to\),这种方式叫做单旋。
但是如果某题的出题人极度不友好,那么单旋操作的时间复杂度就有可能直接起飞。
那么就有了双旋操作。
双旋操作要具体讨论当 前节点 \(-\) 父节点的关系 与 当前节点的父节点 \(-\) 祖父节点的关系 是否相同。
单旋
直接无脑上提某节点即可。
双旋
设访问过的节点为 \(x\) , \(x\) 的父节点为 \(fa[x]\) ,则有三种情况:
-
\(fa[x]\) 为根节点。
将 \(x\) 旋转一次即可。
-
\(fa[x]\) 不是根节点,\(x\) 是 \(fa[x]\) 的左 / 右儿子且 \(fa[x]\) 是 \(fa[fa[x]]\) 的左 / 右儿子。
先右 / 左旋 \(fa[x]\) ,再右 / 左旋 \(x\) 。
-
\(fa[x]\) 不是根节点,\(x\) 是 \(fa[x]\) 的左 / 右儿子且 \(fa[x]\) 是 \(fa[fa[x]]\) 的右 / 左儿子。
先右 / 左旋 \(x\) ,再左 / 右旋 \(x\) 。
容易发现,在直接的链状数据结构上,每次双旋都会产生一个交叉点,而使得某段的高度下降一般从而最后使得整个树的高度减少了一般,最终倒是二叉搜索树趋于*衡。
CODE
Splay(x,to)
表示把 \(x\) 旋转到 \(to\) 的儿子(当 \(g=0\) 时表示旋转到根)
void rotate(int now_)//通过旋转把节点 now_ 上移
{
int fa_ = fa[now_];
bool chson = check_son(now_);
if (fa[fa[now_]])
son[fa[fa[now_]]][check_son(fa[now_])] = now_; //更新祖父
fa[now_] = fa[fa[now_]];
son[fa_][chson] = son[now_][chson ^ 1]; //更新 fa 的儿子
fa[son[fa_][chson]] = fa_;
son[now_][chson ^ 1] = fa_; //上提 now_
fa[fa_] = now_;
push_up(fa_);
push_up(now_);
}
void splay(int now_, int to) //把 now_ 旋转到 to 的儿子的位置(当 to=0 时表示旋转到根)
{
while (fa[now_] != to)
{
int f = fa[now_];
int f_ = fa[f];
if (f_ != to)
rotate((now_ == lc[f]) == (f == lc[f_]) ? f : now_);
rotate(now_);
}
if (!to)
rt = now_; //标记根节点
}
封装模板
By Luckyblock
太爱了学长了鸭!!
GYH 贴贴~
//by Luckyblock
namespace Splay
{
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = 1e6 + 10;
int root, node_num, fa[kMaxNode], son[kMaxNode][2];
int val[kMaxNode], cnt[kMaxNode], siz[kMaxNode];
int top, bin[kMaxNode];
void Pushup(int now_) //更新节点大小信息
{
if (!now_)
return;
siz[now_] = cnt[now_];
if (ls)
siz[now_] += siz[ls];
if (rs)
siz[now_] += siz[rs];
}
int WhichSon(int now_) //获得儿子类型
{
return now_ == son[f][1];
}
void Clear(int now_) //清空节点,同时进行垃圾回收
{
f = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
bin[++top] = now_;
}
int NewNode(int fa_, int val_) //建立新节点
{
int now_ = top ? bin[top--] : ++node_num; //优先调用垃圾堆
f = fa_, val[now_] = val_, siz[now_] = cnt[now_] = 1;
return now_;
}
void Rotate(int now_) //旋转操作
{
int fa_ = f, whichson = WhichSon(now_);
if (fa[f])
son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][whichson] = son[now_][whichson ^ 1];
fa[son[fa_][whichson]] = fa_;
son[now_][whichson ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Splay(int now_) //旋转到根操作
{
for (; f != 0; Rotate(now_))
if (fa[f])
Rotate(WhichSon(now_) == WhichSon(f) ? f : now_);
root = now_;
}
void Insert(int now_, int fa_, int val_) //插入操作
{
if (now_ && val[now_] != val_)
{
Insert(son[now_][val[now_] < val_], now_, val_);
return;
}
if (val[now_] == val_) //已存在
++cnt[now_];
if (!now_) //不存在
{
now_ = NewNode(fa_, val_);
if (f)
son[f][val[f] < val_] = now_;
}
Pushup(now_);
Pushup(f);
Splay(now_); //注意旋转到根
}
int Find(int now_, int val_) //将权值 val 对应节点旋转至根。在*衡树上二分。
{
if (!now_) //不存在
return false;
if (val_ < val[now_])
return Find(ls, val_);
if (val_ == val[now_])
{
Splay(now_);
return true;
}
return Find(rs, val_);
}
void Delete(int val_) //删除一个权值 val
{
if (!Find(root, val_)) //将 val 转到根
return;
if (cnt[root] > 1)
{
--cnt[root];
Pushup(root);
return;
}
int oldroot = root;
if (!son[root][0] && !son[root][1])
root = 0;
else if (!son[root][0])
root = son[root][1], fa[root] = 0;
else if (!son[root][1])
root = son[root][0], fa[root] = 0;
else if (son[root][0] && son[root][1]) //将中序遍历中 root 前的一个元素作为新的 root。该元素即为 root 左子树中最大的元素。
{
int leftmax = son[root][0];
while (son[leftmax][1])
leftmax = son[leftmax][1];
Splay(leftmax); //转到根
son[root][1] = son[oldroot][1];
fa[son[root][1]] = root; //继承信息
}
Clear(oldroot);
Pushup(root);
}
int QueryRank(int val_) //查询 val_ 的排名
{
Insert(root, 0, val_); //先插入,将其转到根,查询左子树大小
int ret = siz[son[root][0]] + 1;
Delete(val_);
return ret;
}
int QueryVal(int rk_) //查询排名为 rk 的权值。在*衡树上二分。
{
int now_ = root;
while (true)
{
if (!now_)
return -1;
if (ls && siz[ls] >= rk_) //注意 =
now_ = ls;
else
{
rk_ -= ls ? siz[ls] : 0;
if (rk_ <= cnt[now_]) //该权值即为 val[now_]
{
Splay(now_);
return val[now_];
}
rk_ -= cnt[now_];
now_ = rs;
}
}
}
int QueryPre(int val_) //查询前驱
{
Insert(root, 0, val_); //插入 val_,将其旋转到根,前驱(中序遍历中前一个元素)即为左子树中最大的元素。
int now_ = son[root][0];
while (rs)
now_ = rs;
Delete(val_);
return val[now_];
}
int QueryNext(int val_) //查询后继
{
Insert(root, 0, val_);
int now_ = son[root][1];
while (ls)
now_ = ls;
Delete(val_);
return val[now_];
}
}
//有删减修改
写在最后
\(Splay\) 并木有就此完结,上述模板中还有很多基本操作,且听下回分解。
鸣谢:
-
百度百科