[关闭]
@exut 2024-11-11T22:34:53.000000Z 字数 2406 阅读 174

splay

DS


终于还是把目光放在 LCT 上面了,那要学LCT这东西也是必须的了

本文学习自yyb大佬的博客,因为大家说yyb是splay第一人,所以我觉得按他的写法肯定死不了

树旋转

一上来就感觉到yyb大佬的写法确实是很厉害的,以前不愿意选splay一个很大的原因就是感觉旋转分讨很麻烦的说,然后今天看到了yyb的奇妙强势写法

单旋

先考虑三个点:

我们不妨考虑现在想要把 旋转到 的位置但是不破坏BST的基本性质

以这种情况为例

首先自然是 要跑到 那里去,其次由于 的左儿子所以现在 应该是 的右儿子, 的左儿子空出来了自然就拿 自己本来的右儿子补上,可以发现这样是对的

找一下规律推广,就是:

  1. 变到原来 的位置
  2. 变成了 原来在 的 相反的那个儿子
  3. 顶上 的与 原来位置相对的那个儿子,并把 自己空缺的那个儿子补上
  1. bool isd(int x){//x是他父亲的哪边儿子
  2. return tr[tr[x].fa].ch[1]==x;
  3. }
  4. void pushup(int x){
  5. tr[x].siz=tr[tr[x].ch[0]].siz+tr[tr[x].ch[1]].siz+tr[x].cnt;
  6. }
  7. void rotate(int x){
  8. int y=tr[x].fa,z=tr[y].fa;
  9. int k=isd(x);
  10. tr[x].fa=z;
  11. tr[z].ch[isd(y)]=x;
  12. tr[y].ch[k]=tr[x].ch[k^1];
  13. tr[tr[x].ch[k^1]].fa=y;
  14. tr[y].fa=x;
  15. tr[x].ch[k^1]=y;
  16. pushup(y),pushup(x);
  17. }

双旋

有一个问题,我们可以手动试一下,假如我们希望把 旋转到根结点,如果一直单旋会不会有什么问题呢

假设你已经试完了,我们会注意到这个时候树的深度并没有变化,这就意味着这个平衡了个寂寞

假设我们先要把 旋转到 没特别说明都保持上文定义)

这个时候有两种情况:

前者我们称为同构调整,后者称为异构

对于同构,我们需要的操作是先旋转 再旋转

异构则是 旋转两次

原因手模即可,什么你不理解,那也没事记着就完了其实我也不理解,也许跟势能分析有关吧

于是我们可以写一个及其简单的splay操作

  1. void splay(int x,int F){//把x旋转到F 的儿子
  2. while(tr[x].fa!=F){
  3. int y=tr[x].fa,z=tr[y].fa;
  4. if(z!=F)
  5. (isd(x)^isd(y))?rotate(x):rotate(y);
  6. rotate(x);
  7. }
  8. if(!F) rot=x;
  9. }

find

查找值为 的元素并且把 的点扔到根

  1. void find(int x){//找到x并旋转到根
  2. int u=rot;
  3. if(!u) return;
  4. while(tr[u].ch[x>tr[u].val] and x!=tr[u].val)
  5. u=tr[u].ch[x>tr[u].val];
  6. splay(u,0);
  7. }

insert

类似于find直接去找这个点,然后看存不存在这个数,如果存在加次数,不存在就直接就建点

这里吐槽一下yyb给的实现里前面的代码忘记pushup了(而且他文字部分说了要pushup)

  1. void ins(int x){
  2. int u=rot,ff=0;
  3. while(u and tr[u].val!=x) ff=u,u=tr[u].ch[x>tr[u].val];
  4. if(u) tr[u].cnt++;
  5. else{
  6. u=++idx;
  7. if(ff) tr[ff].ch[x>tr[ff].val]=u;
  8. tr[u]={0,0,ff,1,1,x};
  9. }
  10. splay(u);
  11. }

前驱后继

实现合在一起, 是前驱 是后继,原理很简单把 find出来,先判断他自己是不是,然后就是小于他的最大的和大于的最小的

  1. int nxt(int x,bool f){
  2. find(x);
  3. int u=rot;
  4. if(tr[u].val>x and f) return u;
  5. if(tr[u].val<x and !f) return u;
  6. u=tr[u].ch[f];
  7. while(tr[u].ch[f^1]) u=tr[u].ch[f^1];
  8. return u;
  9. }

rank

直接find查左子树大小即可

但是注意到find找不到这个点的时候上来这个点可能是 的前驱,这个时候就需要把这个点自己删掉,此处yyb在板子题的代码已经被hack过不了了

  1. int rk(int x){
  2. find(x);
  3. return tr[tr[rot].ch[0]].siz+(tr[rot].val<x)*tr[rot].cnt;
  4. }

kth

常规的平衡树

  1. int kth(int k){
  2. int u=rot;
  3. if(tr[u].siz<k) return -inf;
  4. while(1){
  5. if(tr[tr[u].ch[0]].siz+tr[u].cnt<k)
  6. k-=tr[tr[u].ch[0]].siz+tr[u].cnt,u=tr[u].ch[1];
  7. else if(tr[tr[u].ch[0]].siz>=k) u=tr[u].ch[0];
  8. else return tr[u].val;
  9. }
  10. }

del

可以想象,比一个数的前驱大、后继小的数只有这个数本人

于是这样搞一下就可以把这个数的点单独搞出来,后续类似

  1. void del(int x){
  2. int a=nxt(x,0),b=nxt(x,1);
  3. splay(a),splay(b,a);
  4. int u=tr[b].ch[0];
  5. if(tr[u].cnt>1) tr[u].cnt--,splay(u);
  6. else tr[b].ch[0]=0,pushup(b),pushup(a);
  7. }

我宣布我是splay的狗了!因为我splay跑的比我fhq快!

文艺区间操作

我们考虑用kth找出 的区间和 的区间,把 转到根 转到 的右儿子,这样 的左子树就是

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注