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