无旋 Treap

· · 算法·理论

Treap

看这个词不难想到 Tree(二叉搜索树)和 Heap(堆)。

Treap 就是一种结合了二叉搜索树与堆的优点的一种数据结构。

Treap 通过二叉搜索树维护操作,通过随机堆的值来维持平衡。(如下图)

节点里的数是值,满足二叉搜索树的性质(即左子树所有值均小于当前节点,右子树所有值均大于当前节点)。节点旁黄色的是随机权值(这里为了方便阅读,写成小值),满足堆(本文为小根堆)的性质。

Treap 分为有旋与无旋两种,本文主要介绍无旋 Treap。

无旋 Treap

无旋 Treap 通过分裂与合并操作实现。

这里以P3369 【模板】普通平衡树为例题讲解。

首先是节点,要存储当前点的值以及随机权值,并且维护一个字树大小。

struct Node {
  int s[2]; // 左右儿子
  int v; // 值
  int k; // 随机权值
  int sz; // 子树大小
} tr[N];

以及新建节点

int idx;
int add(int v) { // 新建一个值为 v 的节点并返回编号
  tr[++idx].v = v; 
  tr[idx].k = rand(); // 随机权值
  tr[idx].sz = 1;
  return idx;
}

还有上传

void pushup(int x) {
  tr[x].sz = tr[tr[x].s[0]].sz + tr[tr[x].s[1]].sz + 1;
}

上面的都较为简单不做讲解。

接下来是分裂操作。

现在有一棵以 p 为根的树,要按值分裂成左右两棵,根为 x, y,左边的树的值都小于等于 v,右边的树的值都大于 v

如果 p 的值 \le v,那么说明 p 及其左子树一定在左边的那棵树,且 p 一定是左边的树的根,那么令 x = p,右子树中可能还有 \le v 的,递归处理右子树,并将右子树的分裂出的左边的树的根放到 p 的右儿子。

如果 p 的值 > v,那么说明 p 及其右子树一定在右边的那棵树,且 p 一定是右边的树的根,那么令 y = p,左子树中可能还有 > v 的,递归处理左子树,并将左子树的右边的树放到 p 的左儿子。

可以发现,这样分裂出的两棵树的边一定是初始时的边或者是祖先连向儿子,所以满足堆的性质。

最后更新一下 p 的子树大小即可。

pair<int, int> split(int p, int v) { // pair 表示 x, y
  if (!p) { // 当前节点为空,分裂出的两棵树也都为空
    return make_pair(0, 0);
  }
  pair<int, int> res;
  if (tr[p].v <= v) {
    res = split(tr[x].s[1], v); // 递归右子树
    tr[x].s[1] = res.first; // 将右儿子改为右子树的分裂出的左边的树的根
    res.first = p; // 将 x 改为 p
  } else {
    y = p;
    res = split(tr[y].s[0], v); // 递归左子树 
    tr[y].s[0] = res.second; // 将左儿子改为左子树的分裂出的右边的树的根
    res.second = p; // 将 y 改为 p
  }
  pushup(p); // 更新 p 的 sz
  return res;
}

理解后可以换成下面这种写法

void split(int p, int v, int &x, int &y) { // 这个引用写法类似将 x, y 返回后赋值
  if (!p) { // 当前节点为空,分裂出的两棵树也都为空
    x = 0;
    y = 0;
    return;
  }
  if (tr[p].v <= v) {
    x = p; // 将 x 改为 p
    split(tr[x].s[1], v, tr[x].s[1], y); // 递归右子树,并将右儿子改为右子树的分裂出的左边的树的根
  } else {
    y = p; // 将 y 改为 p
    split(tr[y].s[0], v, x, tr[y].s[0]); // 递归左子树,并将左儿子改为左子树的分裂出的右边的树的根
  }
  pushup(p); // 更新 p 的 sz
}

模拟一下,现在要将下面这棵树按 v = 5 分开(当前根用 rt 表示,最终分裂出的根用 x_0, y_0 表示,这里节点的编号与值相等)。

首先递归 p = 4xx_0yy_0

![](https://cdn.luogu.com.cn/upload/image_hosting/2s4nh3ks.png) 现在 $p = 6$,$x$ 为 $rs(4)$,$y$ 为 $y_0$。 $p$ 的值为 $6 > v = 5$,将 $y$ 设为 $6$,向左儿子递归。 ![](https://cdn.luogu.com.cn/upload/image_hosting/ipipo7qo.png) 现在 $p = 5$,$x$ 为 $rs(4)$,$y$ 为 $ls(6)$。 $p$ 的值为 $5 \le v = 5$,将 $x$ 设为 $5$,向右儿子递归。(这里修改 $rs(4)$ 为 $5$ 是加边并删边) ![](https://cdn.luogu.com.cn/upload/image_hosting/yuysqn0w.png) 现在 $p = 0$,$x$ 为 $rs(5)$,$y$ 为 $ls(6)$。 现在到了空节点,将 $x, y$ 设为 $0$。(这里修改 $ls(6)$ 为 $0$ 是删了边) ![](https://cdn.luogu.com.cn/upload/image_hosting/uevgx6zd.png) 回溯时更新完子树大小。 最后的树就是这样: ![](https://cdn.luogu.com.cn/upload/image_hosting/njh8bcs0.png) --- 接下来是合并操作。 现在有以 $x, y$ 为根的左右两棵树,左边的树的任意值 $\le$ 右边的树的任意值,要按随机权值合并成一棵树,根为 $rt$。 若 $x$ 的权值 $\le y$ 的权值,那么说明 $y$ 应该在 $x$ 的右子树,且 $x$ 应为树的根,向 $x$ 的右儿子递归,将 $x$ 的右儿子改为递归出的根。 若 $x$ 的权值 $> y$ 的权值,那么说明 $x$ 应该在 $y$ 的左子树,且 $x$ 应为树的根,向 $y$ 的左儿子递归,将 $y$ 的左儿子改为递归出的根。 可以发现,这样合并出的树的边一定是初始时的边或者是 $x \to y$ 和 $x \gets y$,所以满足二叉搜索树的性质。 最后更新一下根的子树大小即可。 ```cpp line-numbers int merge(int x, int y) { if (!x || !y) { // 若一个为空,返回另一个 return x + y; } int p; if (tr[x].k < tr[y].k) { p = x; // 将 p 改成 x tr[x].s[1] = merge(tr[x].s[1], y); // 向右儿子递归并修改右儿子 } else { p = y; // 将 p 改成 y tr[y].s[0] = merge(x, tr[y].s[0]); // 向左儿子递归并修改左儿子 } pushup(p); // 更新 p 的 sz return p; // 返回根 } ``` 模拟一下,现在要将下面这两棵树合并(根用 $x_0, y_0$ 表示,最终合并出的根用 $rt$ 表示,这里节点的编号与值相等)。 ![](https://cdn.luogu.com.cn/upload/image_hosting/kfouub2i.png) 首先递归 $x = 4, y = 6$。 $x$ 的随机权值 $\le y$ 的随机权值,将 $p$ 设为 $x = 4$, 向 $x$ 的右儿子递归。 ![](https://cdn.luogu.com.cn/upload/image_hosting/tdbjgk0q.png) 现在 $x = 5, y = 6$。 $x$ 的随机权值 $> y$ 的随机权值,将 $p$ 设为 $y = 4$,向 $y$ 的左儿子递归。(这里修改 $rs(4)$ 为 $6$ 是加边并删边) ![](https://cdn.luogu.com.cn/upload/image_hosting/rjsqzsk7.png) 现在 $x = 5, y = 0$。 $y = 0$,将 $p$ 设为 $x = 5$,终止递归。(这里修改 $rs(4)$ 为 $6$ 是加了边) ![](https://cdn.luogu.com.cn/upload/image_hosting/d43qny8t.png) 回溯时更新完子树大小。 最后的树就是这样: ![](https://cdn.luogu.com.cn/upload/image_hosting/27yzhex2.png) --- 接下来是插入和删除操作。 现在要插入一个值为 $v$ 的点,考虑将树分成两部分,一部分 $x$ 的值 $\le v$,另一部分 $y$ 的值 $> v$,新建一个值为 $v$ 的点 $z$。 此时值的大小关系为 $x \le z < y$,先合并 $x,z$,再合并 $xz, y$,并将 $rt$ 修改。 ```cpp line-numbers void ins(int v) { int x, y, z; split(rt, v, x, y); // 分裂出 x, y z = add(v); // 新建一个节点 z rt = merge(merge(x, z), y); // 合并并更新 rt } ``` 要在下面图中插入一个值为 $5$ 的点(随机权值取 $8$)。 ![](https://cdn.luogu.com.cn/upload/image_hosting/igwgdf4s.png) 分裂。 ![](https://cdn.luogu.com.cn/upload/image_hosting/4hr91s20.png) 合并。 ![](https://cdn.luogu.com.cn/upload/image_hosting/21ylrvln.png) 再合并。 ![](https://cdn.luogu.com.cn/upload/image_hosting/fqfsm2sn.png) 现在要删除一个值为 $v$ 的点,考虑将树先分成两部分,一部分 $xz$ 的值 $\le v$,另一部分 $y$ 的值 $> v$,再将 $xz$ 分成两部分,一部分 $x$ 的值 $\le v - 1$ 即 $< v$,另一部分 $z$ 的值 $> v - 1$ 且 $\le v$ 即 $= v$。 此时直接将 $z$ 的左右儿子合并,即删掉根,删掉一个 $= v$ 的节点。 此时值的大小关系为 $x < z < y$,先合并 $x,z$,再合并 $xz, y$,并将 $rt$ 修改。 ```cpp line-numbers void del(int v) { int x, y, z; split(rt, v, x, y); split(x, v - 1, x, z); z = merge(tr[z].s[0], tr[z].s[1]); rt = merge(merge(x, z), y); } ``` 要在下面图中删除一个值为 $5$ 的点。 ![](https://cdn.luogu.com.cn/upload/image_hosting/w2eii76m.png) 分裂。 ![](https://cdn.luogu.com.cn/upload/image_hosting/q9wazzuu.png) 再分裂。 ![](https://cdn.luogu.com.cn/upload/image_hosting/230gi8mu.png) 将 $z$ 左右儿子合并(删除根)。 ![](https://cdn.luogu.com.cn/upload/image_hosting/hdzfa7r9.png) 合并。 ![](https://cdn.luogu.com.cn/upload/image_hosting/81d0kkc6.png) 再合并。 ![](https://cdn.luogu.com.cn/upload/image_hosting/yskvxqws.png) --- 接下来是一个辅助操作,查询 $p$ 中第 $k$ 小的点的编号。 直接判断是不是当前点,是则返回,否则判断在左还是在右,向儿子递归。 ```cpp line-numbers int getk(int p, int k) { if (k == tr[tr[p].s[0]].sz + 1) { return p; } if (k <= tr[tr[p].s[0]].sz) { return getk(tr[p].s[0], k); } else { return getk(tr[p].s[1], k - tr[tr[p].s[0]].sz - 1); } } ``` --- 接下来是找前驱/后继。 现在找 $v$ 的前驱,考虑将树先分成两部分,一部分 $x$ 的值 $\le v - 1$ 即 $< v$,另一部分 $y$ 的值 $> v - 1$ 即 $\ge v$。 直接找到 $x$ 中第 $sz$ 小的(最大的),输出值即可。 ```cpp line-numbers void getpre(int v) { int x, y; split(rt, v - 1, x, y); int p = getk(x, tr[x].sz); cout << tr[p].v << endl; rt = merge(x, y); } ``` 现在找 $v$ 的后继,考虑将树先分成两部分,一部分 $x$ 的值 $\le v$,另一部分 $y$ 的值 $> v$。 直接找到 $y$ 中第 $1$ 小的(最小的),输出值即可。 ```cpp line-numbers void getsuc(int v) { int x, y; split(rt, v, x, y); int p = getk(y, 1); cout << tr[p].v << endl; rt = merge(x, y); } ``` 接下来是查排名。 现在查 $v$ 的前驱,考虑将树先分成两部分,一部分 $x$ 的值 $\le v - 1$ 即 $< v$,另一部分 $y$ 的值 $> v - 1$ 即 $\ge v$。 $x$ 的 $sz$ 即为 $< v$ 的个数,$+1$ 为排名。 ```cpp line-numbers void getrank(int v) { int x, y; split(rt, v - 1, x, y); cout << tr[x].sz + 1 << endl; rt = merge(x, y); } ``` 接下来是查第 $k$ 小值。 找到第 $k$ 小输出值即可。 ```cpp line-numbers void getv(int k) { int p = getk(rt, k); cout << tr[p].v << endl; } ``` 不难发现上面所有的操作单次复杂度不超过 $\mathcal O(h)$($h$ 为树高)。而随机化让树保持平衡, $h\sim\log n$。 :::success[AC code] ```cpp line-numbers // Problem: P3369 【模板】普通平衡树 // Contest: Luogu // URL: https://www.luogu.com.cn/problem/P3369 // Memory Limit: 128 MB // Time Limit: 1000 ms // // Powered by CP Editor (https://cpeditor.org) #include<bits/stdc++.h> using namespace std; typedef long long ll; #define int ll const int N = 1e5 + 7; int q; struct Node { int s[2]; int v; int k; int sz; } tr[N]; int rt; int idx; int add(int v) { tr[++idx].v = v; tr[idx].k = rand(); tr[idx].sz = 1; return idx; } void pushup(int x) { tr[x].sz = tr[tr[x].s[0]].sz + tr[tr[x].s[1]].sz + 1; } void split(int p, int v, int & x, int & y) { if (!p) { x = 0; y = 0; return; } if (tr[p].v <= v) { x = p; split(tr[x].s[1], v, tr[x].s[1], y); } else { y = p; split(tr[y].s[0], v, x, tr[y].s[0]); } pushup(p); } int merge(int x, int y) { if (!x || !y) { return x + y; } int p; if (tr[x].k < tr[y].k) { p = x; tr[x].s[1] = merge(tr[x].s[1], y); } else { p = y; tr[y].s[0] = merge(x, tr[y].s[0]); } pushup(p); return p; } void ins(int v) { int x, y, z; split(rt, v, x, y); z = add(v); rt = merge(merge(x, z), y); } void del(int v) { int x, y, z; split(rt, v, x, y); split(x, v - 1, x, z); z = merge(tr[z].s[0], tr[z].s[1]); rt = merge(merge(x, z), y); } int getk(int p, int k) { if (k == tr[tr[p].s[0]].sz + 1) { return p; } if (k <= tr[tr[p].s[0]].sz) { return getk(tr[p].s[0], k); } else { return getk(tr[p].s[1], k - tr[tr[p].s[0]].sz - 1); } } void getpre(int v) { int x, y; split(rt, v - 1, x, y); int p = getk(x, tr[x].sz); cout << tr[p].v << endl; rt = merge(x, y); } void getsuc(int v) { int x, y; split(rt, v, x, y); int p = getk(y, 1); cout << tr[p].v << endl; rt = merge(x, y); } void getrank(int v) { int x, y; split(rt, v - 1, x, y); cout << tr[x].sz + 1 << endl; rt = merge(x, y); } void getv(int k) { int p = getk(rt, k); cout << tr[p].v << endl; } signed main() { cin >> q; while (q--) { int op, x; cin >> op >> x; if (op == 1) { ins(x); } if (op == 2) { del(x); } if (op == 3) { getrank(x); } if (op == 4) { getv(x); } if (op == 5) { getpre(x); } if (op == 6) { getsuc(x); } } return 0; } ``` :::