无旋 Treap

· · 算法·理论

Treap

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

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

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

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

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

无旋 Treap

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

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

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

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

以及新建节点

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

还有上传

void pushup(int p) {
  tr[p].sz = tr[tr[p].ls].sz + tr[tr[p].rs].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].rs, v); // 递归右子树
    tr[x].rs = res.first; // 将右儿子改为右子树的分裂出的左边的树的根
    res.first = p; // 将 x 改为 p
  } else {
    y = p;
    res = split(tr[y].ls, v); // 递归左子树 
    tr[y].ls = 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].rs, v, tr[x].rs, y); // 递归右子树,并将右儿子改为右子树的分裂出的左边的树的根
  } else {
    y = p; // 将 y 改为 p
    split(tr[y].ls, v, x, tr[y].ls); // 递归左子树,并将左儿子改为左子树的分裂出的右边的树的根
  }
  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].rs = merge(tr[x].rs, y); // 向右儿子递归并修改右儿子 } else { p = y; // 将 p 改成 y tr[y].ls = merge(x, tr[y].ls); // 向左儿子递归并修改左儿子 } 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 int 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 return z; } ``` 要在下面图中插入一个值为 $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 int del(int v) { int x, y, z; split(rt, v, x, y); // 分裂出 x, y, z split(x, v - 1, x, z); int res = 1; if (!z) { res = 0; } z = merge(tr[z].ls, tr[z].rs); // 删除 z rt = merge(merge(x, z), y);// 合并并更新 rt return res; // 返回是否删除成功 } ``` 要在下面图中删除一个值为 $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].ls].sz) { return getk(tr[p].ls, k); } if (k > tr[tr[p].ls].sz + 1) { return getk(tr[p].rs, k - tr[tr[p].ls].sz - 1); } return p; } ``` --- 接下来是找前驱/后继。 现在找 $v$ 的前驱,考虑将树先分成两部分,一部分 $x$ 的值 $\le v - 1$ 即 $< v$,另一部分 $y$ 的值 $> v - 1$ 即 $\ge v$。 直接找到 $x$ 中第 $sz$ 小的(最大的),返回值即可。 ```cpp line-numbers int getpre(int v) { int x, y; split(rt, v - 1, x, y); int p = getk(x, tr[x].sz); int res = tr[p].v; rt = merge(x, y); return res; } ``` 现在找 $v$ 的后继,考虑将树先分成两部分,一部分 $x$ 的值 $\le v$,另一部分 $y$ 的值 $> v$。 直接找到 $y$ 中第 $1$ 小的(最小的),返回值即可。 ```cpp line-numbers int getsuc(int v) { int x, y; split(rt, v, x, y); int p = getk(y, 1); int res = tr[p].v; rt = merge(x, y); return res; } ``` 接下来是查排名。 现在查 $v$ 的前驱,考虑将树先分成两部分,一部分 $x$ 的值 $\le v - 1$ 即 $< v$,另一部分 $y$ 的值 $> v - 1$ 即 $\ge v$。 $x$ 的 $sz$ 即为 $< v$ 的个数,$+1$ 为排名。 ```cpp line-numbers int getrank(int v) { int x, y; split(rt, v - 1, x, y); int res = tr[x].sz + 1; rt = merge(x, y); return res; } ``` 接下来是查第 $k$ 小值。 找到第 $k$ 小返回值即可。 ```cpp line-numbers int getv(int k) { int p = getk(rt, k); return tr[p].v; } ``` 不难发现上面所有的操作单次复杂度不超过 $\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 ls, rs; int v; int k; int sz; } tr[N]; int rt; int idx; int add(int v) { int p = ++ idx; tr[p].v = v; tr[p].k = rand(); tr[p].sz = 1; return p; } void pushup(int p) { tr[p].sz = tr[tr[p].ls].sz + tr[tr[p].rs].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].rs, v, tr[x].rs, y); } else { y = p; split(tr[y].ls, v, x, tr[y].ls); } 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].rs = merge(tr[x].rs, y); } else { p = y; tr[y].ls = merge(x, tr[y].ls); } pushup(p); return p; } int ins(int v) { int x, y, z; split(rt, v, x, y); z = add(v); rt = merge(merge(x, z), y); return z; } int del(int v) { int x, y, z; split(rt, v, x, y); split(x, v - 1, x, z); int res = 1; if (!z) { res = 0; } z = merge(tr[z].ls, tr[z].rs); rt = merge(merge(x, z), y); return res; } int getk(int p, int k) { if (k <= tr[tr[p].ls].sz) { return getk(tr[p].ls, k); } if (k > tr[tr[p].ls].sz + 1) { return getk(tr[p].rs, k - tr[tr[p].ls].sz - 1); } return p; } int getpre(int v) { int x, y; split(rt, v - 1, x, y); int p = getk(x, tr[x].sz); int res = tr[p].v; rt = merge(x, y); return res; } int getsuc(int v) { int x, y; split(rt, v, x, y); int p = getk(y, 1); int res = tr[p].v; rt = merge(x, y); return res; } int getrank(int v) { int x, y; split(rt, v - 1, x, y); int res = tr[x].sz + 1; rt = merge(x, y); return res; } int getv(int k) { int p = getk(rt, k); return tr[p].v; } 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) { cout << getrank(x) << endl; } if (op == 4) { cout << getv(x) << endl; } if (op == 5) { cout << getpre(x) << endl; } if (op == 6) { cout << getsuc(x) << endl; } } return 0; } ``` ::: --- 可持久化只需要修改分裂和合并就行了。 ```cpp line-numbers void split(int u, int v, int &x, int &y) { if (!u) { x = 0; y = 0; return ; } int t = ++ idx; // 新建一个节点 tr[t] = tr[u]; if (tr[t].v <= v) { x = t; split(tr[u].rs, v, tr[t].rs, y); } else { y = t; split(tr[u].ls, v, x, tr[t].ls); } pushup(t); } ``` ```cpp line-numbers int merge(int x, int y) { if (!x || !y) { return x | y; } int t = ++ idx; // 新建一个节点 if (tr[x].k <= tr[y].k) { tr[t] = tr[x]; tr[t].rs = merge(tr[x].rs, y); } else { tr[t] = tr[y]; tr[t].ls = merge(x, tr[y].ls); } pushup(t); return t; } ``` 其他的都不变,也可以改成纯递归实现。 :::info[AC Code] ```cpp line-numbers // Problem: P3835 【模板】可持久化平衡树 // Contest: Luogu // URL: https://www.luogu.com.cn/problem/P3835 // 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 = 5e5 + 7; int q; struct Node { int ls, rs; int v, k, sz; } tr[N << 6]; int idx, rt[N]; int add(int v) { int p = ++ idx; tr[p].v = v; tr[p].k = rand(); tr[p].sz = 1; return p; } void pushup(int p) { tr[p].sz = tr[tr[p].ls].sz + tr[tr[p].rs].sz + 1; } void split(int p, int v, int &x, int &y) { if (!p) { x = 0; y = 0; return ; } int t = ++ idx; tr[t] = tr[p]; if (tr[t].v <= v) { x = t; split(tr[p].rs, v, tr[t].rs, y); } else { y = t; split(tr[p].ls, v, x, tr[t].ls); } pushup(t); } int merge(int x, int y) { if (!x || !y) { return x | y; } int t = ++ idx; if (tr[x].k <= tr[y].k) { tr[t] = tr[x]; tr[t].rs = merge(tr[x].rs, y); } else { tr[t] = tr[y]; tr[t].ls = merge(x, tr[y].ls); } pushup(t); return t; } int ins(int rt, int v) { int x, y, z; split(rt, v, x, y); z = add(v); rt = merge(merge(x, z), y); return rt; } int del(int rt, int v) { int x, y, z; split(rt, v, x, y); split(x, v - 1, x, z); z = merge(tr[z].ls, tr[z].rs); rt = merge(merge(x, z), y); return rt; } int getk(int p, int k) { if (k <= tr[tr[p].ls].sz) { return getk(tr[p].ls, k); } if (k > tr[tr[p].ls].sz + 1) { return getk(tr[p].rs, k - tr[tr[p].ls].sz - 1); } return p; } int getrank(int &rt, int v) { int x, y; split(rt, v - 1, x, y); int res = tr[x].sz + 1; rt = merge(x, y); return res; } int getv(int &rt, int k) { int p = getk(rt, k); return tr[p].v; } int getpre(int &rt, int v) { int x, y; split(rt, v - 1, x, y); int p = getk(x, tr[x].sz); int res = tr[p].v; rt = merge(x, y); return res; } int getsuc(int &rt, int v) { int x, y; split(rt, v, x, y); int p = getk(y, 1); int res = tr[p].v; rt = merge(x, y); return res; } signed main() { srand(time(0)); cin >> q; for (int i = 1, k, op, x; i <= q; ++ i) { cin >> k >> op >> x; if (op == 1) { rt[i] = ins(rt[k], x); } if (op == 2) { rt[i] = del(rt[k], x); } if (op == 3) { rt[i] = rt[k]; cout << getrank(rt[i], x) << endl; } if (op == 4) { rt[i] = rt[k]; cout << getv(rt[i], x) << endl; } if (op == 5) { rt[i] = rt[k]; cout << getpre(rt[i], x) << endl; } if (op == 6) { rt[i] = rt[k]; cout << getsuc(rt[i], x) << endl; } } return 0; } ``` :::