splay 学习笔记


个人总结向博客。

splay 也满足二叉搜索树性质。

考虑 splay 的旋转操作。

盗一下 @attack 大佬的图片。

分类讨论 \(x\)\(Y\) 的左儿子的情况。

我们现在要做的操作就是将 \(X\) 点转到 \(Y\) 点上去。

那么考虑改变之后树是怎么变的。

我们将 \(Y\) 的父亲改为了 \(X\) ,然后将 \(X\) 的右儿子的父亲改为 \(Y\), 然后将 \(Y\) 的左儿子改为 \(X\) 的右儿子。

简单说就是,爸爸是我的儿子,我的右儿子的父亲是我的爸爸,我的爸爸的左儿子是我的右儿子。

我们现在要做的操作就是将 \(X\) 点转到 \(Y\) 点上去。

那么考虑改变之后树是怎么变的。

我们将 \(Y\) 的父亲改为了 \(X\) ,然后将 \(X\) 的左儿子的父亲改为 \(Y\), 然后将 \(Y\) 的右儿子改为 \(X\) 的左儿子。

简单说就是,爸爸是我的儿子,我的左儿子的父亲是我的爸爸,我的爸爸的右儿子是我的左儿子。

可以选择分别写,但是那样太傻了,你发现这个对于左儿子和右儿子的相对关系之间是一定的,你考虑写一个函数来达到两个函数的功效,不能写的太长。

那么我们设 \(son[u][0/1]\) 表示 \(u\) 的左右儿子标号。

首先我们想要快速确定一个标号是左儿子还是右儿子。

可以直接写出一个函数判断。

  int get(int u) {
    return (son[a[u].fa][1] == u);
  }

那么我们模拟实现上面的代码,当 \(x\)\(y\) 的左儿子的时候。

void rotate(int u) {
  int f1 = a[u].fa, f2 = a[f1].fa, p = get(u);
  //求出我的父亲,我的父亲的父亲,看当前点 $u$ 是左儿子还是右儿子
  if(!f1) return; // 如果没有父亲,那他就是根节点,所以就不用再转了
  if(f2) son[f2][get(f1)] = u; // 有可能我的爸爸是根节点,然后如果我原先的 y 是右儿子,转后 x 也是右儿子,否则就是左儿子
  son[f1][p] = son[u][!p]; a[son[u][!p]].fa = f1; //当 p = 1 时,!p = 0, 当 p = 0 时, !p = 1。我父亲的 左/右 儿子是我的 右/左 儿子
  son[u][!p] = f1; //我原先是我父亲的右儿子,那么转了之后我的父亲会变成我的左儿子,我原先是我的父亲的左儿子,转了之后,我的父亲会变成我的右儿子
  a[u].fa = f2; //将我的父亲设为我父亲的父亲,这步必须要在外面做,因为如果你不更新他的父亲的话就会出现递归调用,如果 f2 变成了 0, 就意味着他转到了根节点
  a[f1].fa = u; //将我的父亲的新的父亲变为我
  push_up(f1), push_up(u); // 更新的顺序不能乱了,必须先计算 f1 的值,然后再更新 u 的值,因为 f1 的值会对 u 产生贡献
}

看着有点小可怕,但是这是因为有注释,而且没压行(

理性压行后的版本:

void rotate(int u) {
  int f1 = a[u].fa, f2 = a[f1].fa, p = get(u);
  if(!f1) return; 
  if(f2) son[f2][get(f1)] = u;
  son[f1][p] = son[u][!p]; a[son[u][!p]].fa = f1; 
  son[u][!p] = f1;  a[u].fa = f2; a[f1].fa = u;
  push_up(f1), push_up(u);
}

然后这就是我们的 splay 中的旋转函数,可以把一个节点与他的爸爸进行位置的互换。

然后我们通过不断地进行这个操作,会让我们的左右子树中一颗高度 \(-1\), 另一颗高度 \(+1\)

最终我们的树会变得平衡。

然后我们的 splay 是怎么避免了退化成一条链的。

核心操作就在于 splay 操作,我们对于每个我们每次查询的点,我们都将他 \(splay\) 到我们的根节点,然后改变树的形态,这样的话他就不会造成说出现被卡成一条链的情况。

我们 splay 的暴力思路是怎样的,就是直接抓住一个点,然后不停的把这个点往上抬对吧。

但是好像能被卡,但是我也没听见被卡过 /qd 。

问题不大,毕竟大部分的平衡树里面都是带一个随机的。

然后考虑优化一下,每次先预判 \(x\) 节点的父亲的方向,如果方向一致就旋转他的父亲节点。

然后其实看着很暴力,雀食很暴力,但是由于经过操作的 splay 的期望树高是 \(\log n\) 的,于是他是对的。

详细的复杂度分析现在先不管了,等后面另开一篇分析,有点小长。

就是这样的。

void splay(int x) {
  while(a[x].fa) { //转到根节点停止
    int p = (get(a[x].fa) == get(x)); //预判是否方向一样
    if(p) rotate(a[x].fa); //一样就转父亲
    else rotate(x); // 否则转儿子
    rotate(x); // 再转一次儿子
  }
}

然后那么对于插入删除这些操作有了这两个核心操作之后就很好写了,然后作者这里用的是迭代写的,可能有一些常数,不过这样好写。

插入的过程就是考虑根据二叉搜索树性质去看插入的值与当前节点的值比谁大,小则插入左子树,大则插入右子树,一样就是直接在这个节点的次数上加一就是了,然后递归做,就很简单了。

int add() {
  ++tot;
  a[tot].fa = son[tot][1] = son[tot][0] = a[tot].cnt = a[tot].siz = 0;//初始化节点
  return tot;
}//新建一个节点
void insert(int u,int x,int fa) {
  if(!u) { // 当前走到了一个空节点可以插入了
    u = add(); 
    if(fa) a[u].fa = fa;
    son[fa][a[fa].val < x] = u; // 判断是左儿子还是右儿子
    a[u].cnt = a[u].siz = 1;
    a[u].val = x;
    splay(u);//将当前点转到根的位置
    return;
  }
  if(a[u].val == x) { a[u].cnt++; a[u].siz++; splay(u); return;}
  insert(son[u][a[u].val < x], x, u);
  return;
}

删除是一样的,但是有可能有一个节点会被我们删空,这个时候咋办,那就是考虑直接暴力合并就行了。

void merge(int x,int y) {
  if(!x || !y) { rt = x + y; return;}
  int u = x;
  while(u) x = u, u = son[u][1];
  splay(x);
  son[x][1] = y; a[y].fa = x;
  push_up(x);
}
void del(int u,int x) {
  if(a[u].val == x) {
    splay(u); a[u].cnt--, a[u].siz--;
    if(!a[u].cnt) {
      a[son[u][0]].fa = a[son[u][1]].fa = 0;
      merge(son[u][0], son[u][1]);
    }
    return;
  }
  del(son[u][a[u].val < x], x);
  push_up(u);
}

然后是要查找排名,直接找到这个点,然后提到根,左子树中的数的个数就是排名比他前的,那么排名就是左子树的大小 \(+1\)

int fin(int u,int x) {
  if(a[u].val == x) { 
    splay(u);
    return a[son[u][0]].siz + 1;
  }
  return fin(son[u][a[u].val < x], x); 
}

然后是查找前驱后继的操作,我们要注意的是,查找前驱后继的时候我们一定要先插入这个节点然后再去找,否则是不行的,因为我们的找前驱是找到当前值这个点,然后前驱是最小的中最大的,于是是他走向左儿子后一直往右儿子走。那么如果没有找到节点就是会运行错误的。

然后后继就是在最大的中最小的,于是他走向右儿子后一直往左儿子走。

最后记得删除插入的节点。

在示例代码中我并没有插入删除,我是在主函数进行的这个过程,请注意

int pre(int x) {
  fin(rt, x);
  int u = son[rt][0], now = rt;
  while(u) now = u, u = son[u][1];
  splay(now);
  return a[now].val;
}
int nxt(int x) {
  fin(rt, x);
  int u = son[rt][1], now = rt;
  while(u) now = u, u = son[u][0];
  splay(now);
  return a[now].val;
}

然后区间第 \(k\) 直接暴力往下找就是了。

int kth(int u,int x) {
  if(a[son[u][0]].siz < x && a[son[u][0]].siz + a[u].cnt >= x) {
    splay(u);
    return a[u].val;
  }
  if(a[son[u][0]].siz >= x) return kth(son[u][0], x);
  return kth(son[u][1], x - a[son[u][0]].siz - a[u].cnt);
}

P3369 【模板】普通平衡树

板子题,照着上面的写就好了。

Code (C++)

#include 
   
   
   
     #define int long long using namespace std; namespace IO { int len = 0; char ibuf[(1 << 20) + 1], *iS, *iT, out[(1 << 25) + 1]; #define gh() \ (iS == iT ? iT = (iS = ibuf) + fread(ibuf, 1, (1 << 20) + 1, stdin), \ (iS == iT ? EOF : *iS++) : *iS++) inline int read() { char ch = gh(); int x = 0; char t = 0; while (ch < '0' || ch > '9') t |= ch == '-', ch = gh(); while (ch >= '0' && ch <= '9') x = x * 10 + (ch ^ 48), ch = gh(); return t ? -x : x; } inline void putc(char ch) { out[len++] = ch; } template 
    
      inline void write(T x) { if (x < 0) putc('-'), x = -x; if (x > 9) write(x / 10); out[len++] = x % 10 + 48; } string getstr(void) { string s = ""; char c = gh(); while (c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == EOF) c = gh(); while (!(c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == EOF))s.push_back(c), c = gh(); return s; } void putstr(string str, int begin = 0, int end = -1) { if (end == -1) end = str.size(); for (int i = begin; i < end; i++) putc(str[i]); return; } inline void flush() { fwrite(out, 1, len, stdout); len = 0; } } // namespace IO using IO::flush; using IO::getstr; using IO::putc; using IO::putstr; using IO::read; using IO::write; const int N = 3e6; int n, rt, tot, son[N][2]; struct node{ int val, fa, siz, cnt;} a[N]; int get(int x) { return son[a[x].fa][1] == x;} void push_up(int x) { a[x].siz = a[x].cnt + a[son[x][1]].siz + a[son[x][0]].siz; } void rotate(int u) { int f1 = a[u].fa, f2 = a[f1].fa, p = get(u); if(!f1) return; if(f2) son[f2][get(f1)] = u; son[f1][p] = son[u][!p]; a[son[u][!p]].fa = f1; son[u][!p] = f1; a[u].fa = f2; a[f1].fa = u; push_up(f1), push_up(u); } void splay(int x) { while(a[x].fa) { int p = (get(a[x].fa) == get(x)); if(p) rotate(a[x].fa); else rotate(x); rotate(x); } rt = x; } int add() { ++tot; a[tot].fa = son[tot][1] = son[tot][0] = a[tot].cnt = a[tot].siz = 0; return tot; } void insert(int u,int x,int fa) { if(!u) { u = add(); if(fa) a[u].fa = fa; son[fa][a[fa].val < x] = u; a[u].cnt = a[u].siz = 1; a[u].val = x; splay(u); return; } if(a[u].val == x) { a[u].cnt++; a[u].siz++; splay(u); return;} insert(son[u][a[u].val < x], x, u); return; } int fin(int u,int x) { if(a[u].val == x) { splay(u); return a[son[u][0]].siz + 1; } return fin(son[u][a[u].val < x], x); } int kth(int u,int x) { if(a[son[u][0]].siz < x && a[son[u][0]].siz + a[u].cnt >= x) { splay(u); return a[u].val; } if(a[son[u][0]].siz >= x) return kth(son[u][0], x); return kth(son[u][1], x - a[son[u][0]].siz - a[u].cnt); } void merge(int x,int y) { if(!x || !y) { rt = x + y; return;} int u = x; while(u) x = u, u = son[u][1]; splay(x); son[x][1] = y; a[y].fa = x; push_up(x); } void del(int u,int x) { if(a[u].val == x) { splay(u); a[u].cnt--, a[u].siz--; if(!a[u].cnt) { a[son[u][0]].fa = a[son[u][1]].fa = 0; merge(son[u][0], son[u][1]); } return; } del(son[u][a[u].val < x], x); push_up(u); } int pre(int x) { fin(rt, x); int u = son[rt][0], now = rt; while(u) now = u, u = son[u][1]; splay(now); return a[now].val; } int nxt(int x) { fin(rt, x); int u = son[rt][1], now = rt; while(u) now = u, u = son[u][0]; splay(now); return a[now].val; } signed main () { n = read(); for(int i = 1; i <= n; i++) { int op = read(), x = read(); if(op == 1) { insert(rt, x, 0); } else if(op == 2) { del(rt, x); } else if(op == 3) { insert(rt, x, 0); write(fin(rt, x)), putc('\n'); del(rt, x); } else if(op == 4) { write(kth(rt, x)), putc('\n'); } else if(op == 5) { insert(rt, x, 0); write(pre(x)), putc('\n'); del(rt, x); } else if(op == 6) { insert(rt, x, 0); write(nxt(x)), putc('\n'); del(rt, x); } } flush(); return 0; } 
     
   

P3391 文艺平衡树

这东西先咕咕咕,等等吧,反正还有挺多东西没讲的。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM