Loading

【算法】平衡树

1. 二叉搜索树

1.1 简介

二叉搜索树是一种二叉树的树形数据结构,能将数据存储在一个树形结构上。

其满足的性质:

  1. 二叉搜索树为一棵二叉树,每个节点至多有 \(2\) 个子节点;
  2. 二叉搜索树中任意一个节点的左儿子值小于本节点值,右儿子值大于本节点值(左右取等亦可);
  3. 二叉搜索树的左右子树均为二叉搜索树。

以上的引申结论有:

  1. 若二叉搜索树的左子树不为空,则其左子树上所有点的值均小于其根节点的值;
  2. 若二叉搜索树的右子树不为空,则其右子树上所有点的值均大于其根节点的值。

基本的二叉搜索树可以完成插入,删除,按值查排名,按排名查值,前驱、后继等等。

1.2 理论

想象如何在二叉搜索树上插入一个数。

考虑在搜索树上二分,每次比较当前插入值与经过节点的值来判断插入的位置。删除亦是如此。

而按值查排名,按排名查值等可以通过维护每个节点子树大小 \(siz\) 来求解。

  1. 按值查排名:类似于插入操作来找到值的位置,若走右儿子,将排名加上左儿子的子树大小加一。

  2. 按排名查值:若左儿子的子树大小大于等于当前排名,则值在左子树;#若右儿子的子树大小小于当前排名,则值在右子树,排名更新为当前排名-左子树-1;

前驱为小于当前值的最大值,后继为大于当前值的最小值。

所以前驱为当前值的排名+1的值,后继为当前值+1的排名的值。

1.3 实现

1.3.1 插入

void add(int x, int v) {
  tree[x].siz++;
  if(tree[x].val == v) {
    tree[x].cnt++;
    return ;
  }
  if(v < tree[x].val) {//左子树
    if(tree[x].ls) {//有左子树
      add(tree[x].ls, v);
    } else {//无左子树
      tree[++num].val = v;
      tree[num].cnt = tree[num].siz = 1;
      tree[x].ls = num;
    }
  }
  else {//右子树
    if(tree[x].rs) {
      add(tree[x].rs, v);
    } else {
      tree[++num].val = v;
      tree[num].cnt = tree[num].siz = 1;
      tree[x].rs = num;
    }
  }
}

删除类似于插入,不过多赘述。

1.3.2 按值查排名

int lrank(int x, int v) {
  if(x == 0) return 0;
  if(v == tree[x].val) {
    return tree[tree[x].ls].siz;
  }
  if(v < tree[x].val) {
    return lrank(tree[x].ls, v);
  }
  return lrank(tree[x].rs, v) + tree[tree[x].ls].siz + tree[x].cnt;
}

1.3.3 按排名查值

int kth(int x, int rk) {
  if(x == 0) return INF;
  if(rk <= tree[tree[x].ls].siz) {
    return kth(tree[x].ls, rk);
  }
  if(rk <= tree[tree[x].ls].siz + tree[x].cnt) {
    return tree[x].val;
  }
  return kth(tree[x].rs, rk - tree[tree[x].ls].siz - tree[x].cnt);
}

1.3.4 前驱

int pre(int x, int v, int ans) {
  if(v <= tree[x].val) {
    if(tree[x].ls) {
      return pre(tree[x].ls, v, ans);
    } else return ans;
  } else {
    if(!tree[x].rs) {
      return (tree[x].val < v) ? tree[x].val : ans;
    }
    if(tree[x].cnt) {
      return pre(tree[x].rs, v, tree[x].val);
    } else return pre(tree[x].rs, v, ans);
  }
}

1.3.5 后继

int nxt(int x, int v, int ans) {
  if(v >= tree[x].val) {
    if(tree[x].rs) {
      return nxt(tree[x].rs, v, ans);
    } else return ans;
  } else {
    if(!tree[x].ls) {
      return (tree[x].val > v) ? tree[x].val : ans;
    }
    if(tree[x].cnt) {
      return nxt(tree[x].ls, v, tree[x].val);
    } else return nxt(tree[x].ls, v, ans);
  }
}

以上为远古代码,码风奇丑轻喷

不难发现,如果数据为顺序插入一条链,那么搜索树也是一条链,这样单次操作的复杂度会升到 \(O(n)\) 级别。所以,平衡树诞生了!平衡树便是用来控制树高在 \(\log n\) 级别左右并且本身为一棵二叉搜索树的数据结构。

2. fhp-treap

2.1 简介

由范浩强巨佬发明的一棵无旋 treap。

2.1.1 何为 treap

\(treap = tree + Heap\)

考虑如何将一棵普通二叉搜索树维护平衡。

很显然,如果每次数据的插入数据随机,那么时间复杂度就能获得均摊的机会。比如按顺序插入 1 2 3 4 5,同样的数据按 3 1 5 2 4 随机插入,树高就变底了,时间复杂度更为优秀。

但是我们无法做到将数据离线下来打乱随机插入,因为可能在期间会涉及到其他的一系列操作,使得最后得到的平衡树和某一时间戳内的不一致。

这时候范浩强巨佬便想到引入索引 key,使得平衡树最后按 key 为大根堆,按 val 为二叉搜索树。在 key 随机的时候,原有的二叉搜索树便能维持一个相对平衡的状态。

如何在二产搜索树的基础上维护堆呢?树旋转!当然我不会啊,于是乎范浩强巨佬又想出了无旋 treap 的写法(%%%。

2.1.2 有无旋转对 treap 的影响

可以这样讲,在我看来:二者常数都一般,无旋的常数要大,但是比有旋好写 N 倍

所以赛场上的我会坚定不移的敲无旋而不是选择难调且耗时的有旋。

2.2 理论

fhq-treap 维护平衡无非就两种操作:分裂(split)与合并(merge)

  1. 分裂:分为按值分与按大小分。

按值分通常当做普通平衡树使用。而按大小分在文艺平衡树中常见。

这里讲解按值分:给定值 val,将一棵 treap 分裂为两棵树,第一棵树上的所有节点的值 \(\leq val\),第二棵树上的所有节点的值 \(> val\),且两树均为 treap。

例如,一棵很随意的树:

image

\(val=9\) 分裂,得到的结果如下图:

image

第一棵子树的根节点为黄色 \(x\),第二棵子树的根节点为蓝色 \(y\)

考虑当前节点 \(p\) 在哪一个树,若 \(p\)\(val\) 小于等于给定 \(val\),说明 \(p\) 及其左子树全部在 \(x\) 树内,然后分裂 \(p\) 的右子树,并寻找 \(p\) 的右子树内合适的 \(p\) 的新右儿子,若 \(p\)\(val\) 大于给定 \(val\),则反之。

  1. 合并:将两 fhq-treap 树合并,前提满足第一棵树的所有值小于第二棵树的所有值。

若第一棵树的当前节点 \(x\)\(key\) 大于第二棵树的当前节点 \(y\)\(key\)。则 \(y\)\(x\) 的右下方(默认 \(x\)\(val\) 小于 \(y\)\(val\))。然后合并 \(x\) 的右子树与 \(y\)

若第一棵树的当前节点 \(x\)\(key\) 小于等于第二棵树的当前节点 \(y\)\(key\)。则 \(y\)\(x\) 的右上方。合并 \(y\) 的左子树与 \(x\)

在合并与分裂时应及时更新子树信息。(pushup)

2.3 实现

2.3.1 分裂 & 合并

void split(int p, int val, int &x, int &y) {
  if(!p) {
    x = y = 0;
    return ;
  }
  if(t[p].val <= val) {
    x = p;
    split(t[p].r, val, t[p].r, y);
  } else {
    y = p;
    split(t[p].l, val, x, t[p].l);
  }
  pushup(p);
}

int merge(int x, int y) {
  if(!x || !y) return x + y;
  if(t[x].key > t[y].key) {
    t[x].r = merge(t[x].r, y);
    pushup(x);
    return x;
  } else {
    t[y].l = merge(x, t[y].l);
    pushup(y);
    return y;
  }
}

2.3.2 插入

考虑新值 \(val\) 会出现在树的那一个位置。可以先将整棵树按照 \(val\) 分裂得到 \(x\)\(y\) 两树,在按顺序依次将 \(x\),新节点,\(y\) 的顺序合并即可(一定要按顺序,因为合并要保证 \(val_x\leq val < val_y\)

void Ins(int val) {
  split(root, val, x, y);
  root = merge(merge(x, Mknode(val)), y);
}

2.3.3 删除

将树按照 \(val\) 分为 \(x, z\) 两树,在将 \(x\) 树按 \(val-1\) 分为 \(x, y\) 两树。因为 \(x\leq val\) 同时分为 \(x<val\)\(y=val\),所以删除的值全在 \(y\) 树内,删一个就行,就将 \(y\) 根的左右儿子合并即可(相当于抛弃根节点)。

裂完记得并回来。

void Del(int val) {
  split(root, val, x, z);
  split(x, val-1, x, y);
  y = merge(t[y].l, t[y].r);
  root = merge(merge(x, y), z)  ;
}

2.3.4 按值查排名

排名的定义是比 \(x\) 小的数的个数加一。

考虑将树按 \(val-1\) 分裂,\(x\) 树的大小加一即为答案。

裂完记得并回来。

int rk(int val) {
  split(root, val-1, x, y);
  int ans = t[x].siz + 1;
  root = merge(x, y);
  return ans;
}

2.3.5 按排名查值

平衡树上二分。

若左儿子的子树大小大于等于当前排名,则值在左子树;#若右儿子的子树大小小于当前排名,则值在右子树,排名更新为当前排名-左子树-1。

所以看得出本操作不基于 fhq-treap 的分裂与合并

int kth(int x) {
  int p = root;
  while(p) {
    if(t[t[p].l].siz + 1 == x) return t[p].val;
    else if(t[t[p].l].siz >= x) p = t[p].l;
    else x -= (t[t[p].l].siz + 1), p = t[p].r; 
  }
}

2.3.6 前驱

前驱的定义为小于 \(x\) 中的最大的数。

将树分裂按 \(val-1\) 分裂,在 \(x\) 树中不停地跳右子树,直到不能跳了为止(无右儿子节点)。此点值即为答案。

int pre(int val) {
  split(root, val-1, x, y);
  int p = x;
  while(t[p].r) p = t[p].r;
  int ans = t[p].val;
  root = merge(x, y);
  return ans;
}

2.3.7 后继

后继的定义为大于 \(x\) 中的最小的数。

将树分裂按 \(val\) 分裂,在 \(y\) 树中不停地跳左子树,直到不能跳了为止(无左儿子节点)。此点值即为答案。

int pre(int val) {
  split(root, val-1, x, y);
  int p = x;
  while(t[p].r) p = t[p].r;
  int ans = t[p].val;
  root = merge(x, y);
  return ans;
}

2.3.8 fhq-treap 模版

#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007

using namespace std;

inline int read() {
  rint x=0,f=1;char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
  return x*f;
}

void print(int x){
  if(x<0){putchar('-');x=-x;}
  if(x>9){print(x/10);putchar(x%10+'0');}
  else putchar(x+'0');
  return;
}

const int N = 1e5 + 10;

struct Node {
  int l, r, val, key, siz;
} t[N];

int n, cnt, root;

mt19937 rd(114514);

int Mknode(int val) {//建立新节点
  t[++cnt].val = val;
  t[cnt].key = rd();
  t[cnt].siz = 1;
  return cnt;
}

void pushup(int p) {//上传信息
  t[p].siz = t[t[p].l].siz + t[t[p].r].siz + 1; 
  return ;
}

void split(int p, int val, int &x, int &y) {
  if(!p) {
    x = y = 0;
    return ;
  }
  if(t[p].val <= val) {
    x = p;
    split(t[p].r, val, t[p].r, y);
  } else {
    y = p;
    split(t[p].l, val, x, t[p].l);
  }
  pushup(p);
}

int merge(int x, int y) {
  if(!x || !y) return x + y;
  if(t[x].key > t[y].key) {
    t[x].r = merge(t[x].r, y);
    pushup(x);
    return x;
  } else {
    t[y].l = merge(x, t[y].l);
    pushup(y);
    return y;
  }
}

int x, y, z;

void Ins(int val) {
  split(root, val, x, y);
  root = merge(merge(x, Mknode(val)), y);
}

void Del(int val) {
  split(root, val, x, z);
  split(x, val-1, x, y);
  y = merge(t[y].l, t[y].r);
  root = merge(merge(x, y), z)  ;
}

int rk(int val) {
  split(root, val-1, x, y);
  int ans = t[x].siz + 1;
  root = merge(x, y);
  return ans;
}

int kth(int x) {
  int p = root;
  while(p) {
    if(t[t[p].l].siz + 1 == x) return t[p].val;
    else if(t[t[p].l].siz >= x) p = t[p].l;
    else x -= (t[t[p].l].siz + 1), p = t[p].r; 
  }
}

int pre(int val) {
  split(root, val-1, x, y);
  int p = x;
  while(t[p].r) p = t[p].r;
  int ans = t[p].val;
  root = merge(x, y);
  return ans;
}

int nxt(int val) {
  split(root, val, x, y);
  int p = y;
  while(t[p].l) p = t[p].l;
  int ans = t[p].val;
  root = merge(x, y);
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n;
  while(n--) {
    int op, x; cin >> op >> x;
    if(op == 1) Ins(x);
    else if(op == 2) Del(x);
    else if(op == 3) cout << rk(x) << '\n';
    else if(op == 4) cout << kth(x) << '\n';
    else if(op == 5) cout << kth(rk(x) - 1) << '\n';
    else cout << kth(rk(x + 1)) << '\n';
  }
  return 0;
}

拿走不谢!

posted @ 2024-07-09 23:43  Daniel_yzy  阅读(16)  评论(0编辑  收藏  举报