学习笔记:Splay
之前学习 Treap 的时候理解的不是太好,堆性质和 BST 性质结合得不是很好。发现后续知识对于 Splay 是需要掌握的,于是心血来潮学习了 Splay,个人感觉理解的比 Treap 好,以后手写平衡树就用 Splay 啦。
模板题:Splay
开点
比较类似 Treap,只不过不需要随机啦。特殊需要记录的是每个节点的 \(fa\),这样方便在旋转和插入中使用。(\(ch[x][0]\) 表示左儿子,\(ch[x][1]\) 表示右儿子)
int create(int v,int fs)
{
val[++tot] = v,fa[tot] = fs;
ch[tot][0] = ch[tot][1] = 0;
siz[tot] = cnt[tot] = 1;
return tot;
}
旋转
和 Treap 的旋转类似,手动模拟。一共是三对关系(注:\(x\) 的父亲为 \(ff\),\(ff\) 的父亲为 \(gf\),\(getwhich\) 求节点的儿子方向):
- \(x\) 的异向儿子变为 \(ff\) 的与 \(x\) 同向的儿子。
- \(x\) 变为 \(gf\) 的与 \(ff\) 同向的儿子。
- \(ff\) 变为 \(x\) 的异向儿子。
注意更新时,既要更新 \(ch\) 数组,还要更新 \(fa\) 数组。最后记得更新节点信息。
void rotate(int x)
{
int ff = fa[x],gf = fa[ff];
int k = getwhich(x),tmp = ch[x][k ^ 1];
fa[tmp] = ff,ch[ff][k] = tmp;
fa[x] = gf;
if(gf) ch[gf][getwhich(ff)] = x;
fa[ff] = x,ch[x][k ^ 1] = ff;
maintain(x),maintain(ff);
}
Splay
关键的操作,其作用就是把节点 \(x\) 通过旋转变成根。不断旋转即可,注意特殊情况是如果 \(x\) 与 \(ff\) 的儿子方向同向,需要先转 \(ff\),达到消链的效果,请自行画图 qaq。
void splay(int x)
{
for(int ff = fa[x];ff = fa[x];rotate(x))
if(fa[ff]) rotate(getwhich(ff) == getwhich(x) ? ff : x);
root = x;
}
插入
如果是空树,那么新建节点并设为根。否则的话根据 BST 性质找到应该插入的地方新建节点,最后通过 Splay 操作把新建的节点转到根位置,把不要忘记更新节点信息。
void ins(int v)
{
if(not root)
{
root = create(v,0);
return ;
}
int cur = root,ff = 0;
while(1)
{
if(val[cur] == v)
{
cnt[cur]++;
maintain(cur),maintain(ff);
splay(cur); break;
}
ff = cur;
cur = ch[cur][val[cur] < v];
if(not cur)
{
ch[ff][val[ff] < v] = create(v,ff);
maintain(ff);
splay(tot); break;
}
}
}
查询排名为 \(k\) 的值
类似于 Treap,从根开始寻找,利用 \(siz\) 和 \(cnt\) 判断该排名是在左子树、当前节点还是右子树,如果是左右子树需向左右儿子移动。注意右子树需要把排名减掉左子树的节点数和当前节点的 \(cnt\),实现见代码,自行理解一下呀。
int bargain(int k)
{
int cur = root;
while(1)
{
if(k <= siz[ch[cur][0]]) cur = ch[cur][0];
else if(siz[ch[cur][0]] + cnt[cur] >= k) return val[cur];
else
{
k -= siz[ch[cur][0]] + cnt[cur];
cur = ch[cur][1];
}
}
}
查询值为 \(v\) 的排名
和上面的操作是类似的,自行理解。注意的是,最后为了方便删除,需要把该节点通过 Splay 旋转到根上。(因为这样能简化删除,直接删根就可以了)
int rankle(int v)
{
int cur = root,ans = 0;
while(1)
{
if(not cur) return ans + 1;
if(v < val[cur]) cur = ch[cur][0];
else if(v == val[cur])
{
ans += siz[ch[cur][0]];
splay(cur);
return ans + 1;
}
else
{
ans += siz[ch[cur][0]] + cnt[cur];
cur = ch[cur][1];
}
}
}
求前驱和后继
这两个操作是类似的,这里是直接对于根进项操作。我们通过将查询值插入,通过 ins 里面的 Splay 把它转到根上,最后再删掉它就可以啦。两个操作是相反的,不是很难理解。
int charge()
{
int cur = ch[root][0];
while(ch[cur][1]) cur = ch[cur][1];
return cur;
}
int recoil()
{
int cur = ch[root][1];
while(ch[cur][0]) cur = ch[cur][0];
return cur;
}
删除
这个好好讲讲。首先需要注意,这里我们删除的是值为 \(v\) 的节点,而不是编号(易错)。我们通过 rankle 找到这个值为 \(v\) 的节点并把它转到根上,这样我们删根就可以了。分情况讨论:
- 如果要删的节点在平衡树中有不止一个,直接减小删除的节点的 \(cnt\) 即可。
- 如果根既没有左儿子,又没有右儿子,那我们直接把树变成空树。(\(root=0\))
- 如果只没有左儿子,那么我们把根设为右儿子即可,整棵树变成了右子树。
- 同理,如果只没有右儿子,我们把根设为左儿子,整棵树变成了左子树。
- 最后一种情况,如果两个儿子都有,那么我们考虑,找到这个节点的前驱,通过 Splay 把它转到根上,同时提前记录原先根(即删除节点)的右儿子,令现在的根和这个记录的节点建立右向的父子关系即可。
注意更新节点信息。
void reliese(int v)
{
rankle(v);
if(cnt[root] > 1)
{
cnt[root]--; maintain(root);
return ;
}
else if(not ch[root][0] and not ch[root][1])
{
root = 0;
return ;
}
else if(not ch[root][0])
{
root = ch[root][1];
fa[root] = 0;
return ;
}
else if(not ch[root][1])
{
root = ch[root][0];
fa[root] = 0;
return ;
}
int tmp = ch[root][1],pre = charge();
splay(pre);
fa[tmp] = root;
ch[root][1] = tmp;
maintain(root);
}
完整代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 100000 + 10;
const int inf = 1 << 29;
int n,tot,root;
int val[N],fa[N],cnt[N],ch[N][2],siz[N];
int create(int v,int fs)
{
val[++tot] = v,fa[tot] = fs;
ch[tot][0] = ch[tot][1] = 0;
siz[tot] = cnt[tot] = 1;
return tot;
}
void maintain(int x)
{
siz[x] = siz[ch[x][0]] + siz[ch[x][1]] + cnt[x];
}
int getwhich(int x)
{
return x == ch[fa[x]][1];
}
void rotate(int x)
{
int ff = fa[x],gf = fa[ff];
int k = getwhich(x),tmp = ch[x][k ^ 1];
fa[tmp] = ff,ch[ff][k] = tmp;
fa[x] = gf;
if(gf) ch[gf][getwhich(ff)] = x;
fa[ff] = x,ch[x][k ^ 1] = ff;
maintain(x),maintain(ff);
}
void splay(int x)
{
for(int ff = fa[x];ff = fa[x];rotate(x))
if(fa[ff]) rotate(getwhich(ff) == getwhich(x) ? ff : x);
root = x;
}
void ins(int v)
{
if(not root)
{
root = create(v,0);
return ;
}
int cur = root,ff = 0;
while(1)
{
if(val[cur] == v)
{
cnt[cur]++;
maintain(cur),maintain(ff);
splay(cur); break;
}
ff = cur;
cur = ch[cur][val[cur] < v];
if(not cur)
{
ch[ff][val[ff] < v] = create(v,ff);
maintain(ff);
splay(tot); break;
}
}
}
int bargain(int k)
{
int cur = root;
while(1)
{
if(k <= siz[ch[cur][0]]) cur = ch[cur][0];
else if(siz[ch[cur][0]] + cnt[cur] >= k) return val[cur];
else
{
k -= siz[ch[cur][0]] + cnt[cur];
cur = ch[cur][1];
}
}
}
int rankle(int v)
{
int cur = root,ans = 0;
while(1)
{
if(not cur) return ans + 1;
if(v < val[cur]) cur = ch[cur][0];
else if(v == val[cur])
{
ans += siz[ch[cur][0]];
splay(cur);
return ans + 1;
}
else
{
ans += siz[ch[cur][0]] + cnt[cur];
cur = ch[cur][1];
}
}
}
int charge()
{
int cur = ch[root][0];
while(ch[cur][1]) cur = ch[cur][1];
return cur;
}
int recoil()
{
int cur = ch[root][1];
while(ch[cur][0]) cur = ch[cur][0];
return cur;
}
void reliese(int v)
{
rankle(v);
if(cnt[root] > 1)
{
cnt[root]--; maintain(root);
return ;
}
else if(not ch[root][0] and not ch[root][1])
{
root = 0;
return ;
}
else if(not ch[root][0])
{
root = ch[root][1];
fa[root] = 0;
return ;
}
else if(not ch[root][1])
{
root = ch[root][0];
fa[root] = 0;
return ;
}
int tmp = ch[root][1],pre = charge();
splay(pre);
fa[tmp] = root;
ch[root][1] = tmp;
maintain(root);
}
signed main()
{
scanf("%lld",&n);
//build();
for(int i = 1;i <= n;i++)
{
int opt,x;
scanf("%lld%lld",&opt,&x);
if(opt == 1) ins(x);
else if(opt == 2) reliese(x);
else if(opt == 3) printf("%lld\n",rankle(x));
else if(opt == 4) printf("%lld\n",bargain(x));
else if(opt == 5)
{
ins(x);
printf("%lld\n",val[charge()]);
reliese(x);
}
else if(opt == 6)
{
ins(x);
printf("%lld\n",val[recoil()]);
reliese(x);
}
}
return 0;
}

浙公网安备 33010602011771号