概述
一种平衡树。有人说它常数大,有人说它比
S
p
l
a
y
\rm Splay
Splay 快。或许两者都对。
优点
好写。好想。简单。
有关它的一个故事:最强的 H a n d I n D e v i l \sf HandInDevil HandInDevil 第一天说:“有 AVL \text{AVL} AVL 还学什么 Treap \text{Treap} Treap 啊!” 第二天说:“ Treap \text{Treap} Treap 打着好简单啊,为什么还要学其他平衡树啊?”
思想
名字の内涵
不难发现 Treap = Tree + Heap \text{Treap}=\text{Tree}+\text{Heap} Treap=Tree+Heap
对于键值 key \text{key} key 来说,和其他所有平衡树一样,中序遍历单调不降。
对于我们自己定义的值 prio \text{prio} prio 来说,它满足堆性质:父节点大于子节点。
平衡方式
对于任何平衡树,最重要的就是它怎么保证高度是
O
(
log
n
)
\mathcal O(\log n)
O(logn) 的。伸展树并不是平衡树。
由于它同时具有堆性质和二叉搜索树性质, T r e a p \rm Treap Treap 实际上是一个 笛卡尔树。如果我们先把节点按照键值 k e y \rm key key 排序,那么树根就是当前区间优先级 p r i o \rm prio prio 的最大值点,然后两个儿子是被分割出的子区间递归建树。
第 i i i 个点是第 j j j 个点的祖先,当且仅当第 i i i 个点的管控区间包含 j j j,即 [ i , j ] [i,j] [i,j] 中 p r i o \rm prio prio 的最大值是 i i i 。概率显然是 1 j − i + 1 \frac{1}{j-i+1} j−i+11 。那么一个点的期望深度就是 ∑ j = 1 n 1 ∣ i − j ∣ + 1 = O ( ln n ) \sum_{j=1}^{n}\frac{1}{|i-j|+1}=\mathcal O(\ln n) ∑j=1n∣i−j∣+11=O(lnn),所以树高就是期望 O ( ln n ) \mathcal O(\ln n) O(lnn) 的。
当然,对于一组给定的 p r i o \rm prio prio,你会发现 T r e a p \rm Treap Treap 的形态是唯一的。
怎么做
使用旋转来调整树的形态。
版本一:旋转
节点定义
像这样——
template < typename T >
struct Node{
T data;
unsigned cnt, size;
int prio;
Node *son[2];
Node(const T &__data):data(__data){
prio = rand(), cnt = size = 1;
son[0] = son[1] = nullptr;
}
bool operator < (const Node &that) const {
return prio < that.prio;
}
void maintain(){
size = cnt;
if(son[0] != nullptr)
size += son[0]->size;
if(son[1] != nullptr)
size += son[1]->size;
}
};
节点更新
更新节点上的信息。可视作树形 D P \Bbb{DP} DP 。
void pushUp(Node* o){
o->size = o->cnt;
if(o->son[0] != NULL)
o->size += o->son[0]->size;
if(o->son[1] != NULL)
o->size += o->son[1]->size;
}
旋转
和 Splay \text{Splay} Splay 比较像,但是没有 Zig-Zig \text{Zig-Zig} Zig-Zig 这些东西。直接旋转即可。
图示为一种旋转。读者不妨自己验证新的树是否仍然满足
key
\text{key}
key 中序遍历不降。
void rotate(Node* &o,int d){
Node* k = o->son[d]; // 谁要篡位
o->son[d] = k->son[d^1];
// 相当于图中的B的叛逃
if(o->son[d] != NULL)
o->son[d]->fa = o;
k->son[d^1] = o; // 儿子今天当爹啦!
k->fa = o->fa, o->fa = k;
pushUp(o), pushUp(k);
o = k; // 用引用传递,可以直接改变当前点
}
插入
首先,按照普通二叉查找树的方法,找到 x x x 应该在的地方。
递归返回时,检查是否满足堆性质,不满足则旋转。
void insert(T __data,unsigned val,Node* &o){
if(o == nullptr){
o = new Node(__data);
o->cnt = o->size = val;
return ;
}
if(o->data == __data){
o->cnt += val, o->size += val;
return ;
}
int d = o->data < __data;
insert(__data,val,o->son[d]);
if(*o < *(o->son[d]))
rotate(o,d);
o->maintain();
}
删除
首先,找到它。如果删掉后不影响树的结构(个数大于 1 1 1 或者是叶子节点),就直接干掉。
否则,如果只有一个儿子,把这个儿子旋转上来;如果有两个儿子, prio \text{prio} prio 大的儿子上来。
然后递归删除即可。
void erase(T __data,unsigned val,Node* &o){
if(o == nullptr)
return ; // fail to erase
int d;
if(o->data == __data){
if(o->cnt > val){
o->cnt -= val, o->size -= val;
return ;
}
if(o->son[0] == nullptr and o->son[1] == nullptr){
delete o; o = nullptr; return ;
}
if(o->son[0] == nullptr or o->son[1] == nullptr)
d = (o->son[1] != nullptr);
else d = (*(o->son[0]) < *(o->son[1]));
rotate(o,d);
erase(__data,val,o->son[d^1]);
}
else{
d = o->data < __data;
erase(__data,val,o->son[d]);
}
o->maintain();
}
排名查询
unsigned getRank(T __data){
unsigned res = 0; Node* o=root;
for(int d; o!=nullptr; o=o->son[d]){
d = o->data < __data or o->data == __data;
unsigned lsize = o->son[0] != nullptr ? o->son[0]->size : 0;
res += d*(lsize+o->cnt);
if(o->data == __data){ res -= (o->cnt-1); break; }
}
return res;
}
查询排名为 k k k 的元素
T kthElement(unsigned k){ Node* o;
for(o=root; o!=nullptr; ){
unsigned lsize = (o->son[0] == nullptr ? 0 : o->son[0]->size);
if(k > lsize){
k -= lsize;
if(k <= o->cnt) break;
k -= o->cnt, o = o->son[1];
}else o = o->son[0];
}
return o->data;
}
询问个数
本质就是要找到那个节点。
unsigned count(T __data){ Node* o;
for(o=root; o!=nullptr; o=o->son[o->data < __data])
if(o->data == __data) break;
return o->cnt;
}
完整代码
template < typename T >
class Treap{
struct Node{
T data;
unsigned cnt, size;
int prio;
Node *son[2];
Node(const T &__data):data(__data){
prio = rand(), cnt = size = 1;
son[0] = son[1] = nullptr;
}
bool operator<(const Node &that)const{
return prio < that.prio;
}
void maintain(){
size = cnt;
if(son[0] != nullptr)
size += son[0]->size;
if(son[1] != nullptr)
size += son[1]->size;
}
};
Node* root;
void rotate(Node* &o,int d){
Node* k = o->son[d];
o->son[d] = k->son[d^1];
k->son[d^1] = o;
o->maintain(), k->maintain();
o = k; // change root
}
void insert(T __data,unsigned val,Node* &o){
if(o == nullptr){
o = new Node(__data);
o->cnt = o->size = val;
return ;
}
if(o->data == __data){
o->cnt += val, o->size += val;
return ;
}
int d = o->data < __data;
insert(__data,val,o->son[d]);
if(*o < *(o->son[d]))
rotate(o,d);
o->maintain();
}
void erase(T __data,unsigned val,Node* &o){
if(o == nullptr)
return ; // err to erase
int d;
if(o->data == __data){
if(o->cnt > val){
o->cnt -= val, o->size -= val;
return ;
}
if(o->son[0] == nullptr and o->son[1] == nullptr){
delete o; o = nullptr; return ;
}
if(o->son[0] == nullptr or o->son[1] == nullptr)
d = (o->son[1] != nullptr);
else d = (*(o->son[0]) < *(o->son[1]));
rotate(o,d);
erase(__data,val,o->son[d^1]);
}
else{
d = o->data < __data;
erase(__data,val,o->son[d]);
}
o->maintain();
}
void dispose(Node* &o){
if(o == nullptr)
return ;
dispose(o->son[0]);
dispose(o->son[1]);
delete o; o = nullptr;
}
public:
void dispose(){
dispose(root);
}
void clear(){
root = nullptr;
}
Treap(){ clear(); }
void insert(T __data,unsigned val=1){
insert(__data,val,root);
}
void erase(T __data,unsigned val=1){
erase(__data,val,root);
}
unsigned getRank(T __data){
unsigned res = 0; Node* o=root;
for(int d; o!=nullptr; o=o->son[d]){
d = o->data < __data or o->data == __data;
unsigned lsize = o->son[0] != nullptr ? o->son[0]->size : 0;
res += d*(lsize+o->cnt);
if(o->data == __data){ res -= (o->cnt-1); break; }
}
return res;
}
T kthElement(unsigned k){ Node* o;
for(o=root; o!=nullptr; ){
unsigned lsize = (o->son[0] == nullptr ? 0 : o->son[0]->size);
if(k > lsize){
k -= lsize;
if(k <= o->cnt) break;
k -= o->cnt, o = o->son[1];
}else o = o->son[0];
}
return o->data;
}
unsigned count(T __data){ Node* o;
for(o=root; o!=nullptr; o=o->son[o->data < __data])
if(o->data == __data) break;
return o->cnt;
}
void monitor(){
printf("tree:");
deBug(root);
putchar('\n');
}
};
版本二:无旋
只需要两个核心操作:分裂与合并。
我在写无旋 Treap \text{Treap} Treap 的指针版本时心态爆炸了。然后就换成数组了。
节点定义
然鹅并没有节点。
int prio[N], data[N];
int son[N][2], root;
unsigned size[N], cnt[N];
vector<int> pool; // 内存池
void newTreap(){
for(int i=1; i<N; ++i)
pool.push_back(i);
root = cnt[0] = size[0] = prio[0] = 0;
// 0 等效于 NULL
}
int newNode(int x){
int id = pool.back();
pool.pop_back();
data[id] = x, size[id] = cnt[id] = 1;
son[id][0] = son[id][1] = 0, prio[id] = rand();
return id;
}
void deleteNode(int id){
pool.push_back(id);
}
分裂
按照某个 key \text{key} key 值 x x x 进行分裂。返回值为 pair \text{pair} pair,左边的树满足 key ≤ x \text{key}\le x key≤x,另一边则相反。
只需要考虑根节点分到了哪一边。如果根节点是右边的,显然其右子树也在右边;如果根节点是左边的,显然其左子树也在左边。
递归的合并另一边(没有跟根节点一起走的子树)。
pair<int,int> split(int bound,int o){
// (x <= bound) and (bound < x)
if(not o) return make_pair(o,o);
pair<int,int> p;
if(data[o] <= bound){
p = split(bound,son[o][1]);
son[o][1] = p.first;
p.first = o;
}
else{
p = split(bound,son[o][0]);
son[o][0] = p.second;
p.second = o;
}
pushUp(o); return p;
}
合并
假设左边的树的 key \text{key} key 严格小于右边的树的 key \text{key} key 。
直接考虑谁是根节点——可以用 prio \text{prio} prio 判断。剩下的事情就是递归了——要满足 key \text{key} key 中序遍历不降。
int merge(int a,int b){
if(not a or not b) return a+b;
if(prio[a] < prio[b]) son[b][0] = merge(a,son[b][0]);
else son[a][1] = merge(son[a][1],b);
pushUp(a), pushUp(b);
return prio[a] < prio[b] ? b : a;
}
为了让代码更短的函数:
void decrease(int o){
-- cnt[o], -- size[o];
}
void increase(int o){
++ cnt[o], ++ size[o];
}
插入
直接把树裂开,再暴力合并两次。
void insert(int x){
pair<int,int> rPart = split(x,root);
pair<int,int> lPart = split(x-1,rPart.first);
if(lPart.second) increase(lPart.second);
else lPart.second = newNode(x);
root = merge(merge(lPart.first,lPart.second),rPart.second);
}
删除
还是把树裂开,剩下的合并上去。
void erase(int x){
pair<int,int> rPart = split(x,root);
pair<int,int> lPart = split(x-1,rPart.first);
if(cnt[lPart.second] > 1) decrease(lPart.second);
else deleteNode(lPart.second), lPart.second = 0;
root = merge(merge(lPart.first,lPart.second),rPart.second);
}
其他函数
unsigned getRank(int x){
// 前驱的排名;(前驱<=x)
unsigned res = 0;
for(int o=root,d; o; o=son[o][d]){
d = data[o] <= x;
res += d*(cnt[o]+size[son[o][0]]);
if(data[o] == x){ res += 1-cnt[o]; break; }
}
return res;
}
int kthElement(unsigned x){ int o;
for(o=root; o; ){
if(x > size[son[o][0]]){
x -= size[son[o][0]];
if(x <= cnt[o]) break;
x -= cnt[o], o = son[o][1];
}else o = son[o][0];
}
return data[o];
}
unsigned count(int x){ int o;
for(o=root; o; o=son[o][data[o]<x])
if(data[o] == x) break;
return cnt[o];
}
完整代码
# define T int // 可以换成你自己需要的类型
const int MaxN = 200000;
namespace Treap{
int prio[MaxN]; T data[MaxN];
int son[MaxN][2], root;
unsigned size[MaxN], cnt[MaxN];
vector<int> pool;
void newTreap(){
for(int i=1; i<MaxN; ++i)
pool.push_back(i);
root = cnt[0] = size[0] = prio[0] = 0;
}
int newNode(T __data){
int id = pool.back();
pool.pop_back();
data[id] = __data;
size[id] = cnt[id] = 0;
son[id][0] = son[id][1] = 0;
prio[id] = rand();
return id;
}
void deleteNode(int id){
pool.push_back(id);
}
void pushUp(int o){
size[o] = cnt[o]+size[son[o][0]]+size[son[o][1]];
}
void change(int o,int addv){
cnt[o] += addv, size[o] += addv;
}
pair<int,int> split(T bound,int o){
// (x <= bound) and (bound < x)
if(not o) return make_pair(0,0);
pair<int,int> p;
if(data[o] <= bound){
p = split(bound,son[o][1]);
son[o][1] = p.first, p.first = o;
}
else{
p = split(bound,son[o][0]);
son[o][0] = p.second, p.second = o;
}
pushUp(o); return p;
}
int merge(int a,int b){
if(not a or not b) return a+b;
if(prio[a] < prio[b]) son[b][0] = merge(a,son[b][0]);
else son[a][1] = merge(son[a][1],b);
pushUp(a), pushUp(b);
return prio[a] < prio[b] ? b : a;
}
void insert(T __data,unsigned val=1){
pair<int,int> rPart = split(__data,root);
pair<int,int> lPart = split(__data-1,rPart.first);
if(not lPart.second) lPart.second = newNode(__data);
change(lPart.second,val);
root = merge(merge(lPart.first,lPart.second),rPart.second);
}
void erase(T __data,unsigned val=1){
pair<int,int> rPart = split(__data,root);
pair<int,int> lPart = split(__data-1,rPart.first);
if(cnt[lPart.second] > val) change(lPart.second,-val);
else deleteNode(lPart.second), lPart.second = 0;
root = merge(merge(lPart.first,lPart.second),rPart.second);
}
unsigned getRank(T __data){
// 前驱的排名;(前驱<=x)
unsigned res = 0;
for(int o=root,d; o; o=son[o][d]){
d = data[o] <= __data;
res += d*(cnt[o]+size[son[o][0]]);
if(data[o] == __data){ res += 1-cnt[o]; break; }
}
return res;
}
T kthElement(unsigned x){ int o;
for(o=root; o; ){
if(x > size[son[o][0]]){
x -= size[son[o][0]];
if(x <= cnt[o]) break;
x -= cnt[o], o = son[o][1];
}else o = son[o][0];
}
return data[o];
}
unsigned count(T __data){ int o;
for(o=root; o; o=son[o][data[o] < __data])
if(data[o] == __data) break;
return cnt[o];
}
};
大小版
显然 T r e a p \rm Treap Treap 的 s p l i t \rm split split 也可以基于 s i z e size size 。再实现求排名与求第 k k k 小之后,就可以实现所有功能了。
namespace Treap{
mt19937 rander;
const int MaxM = 500005;
unsigned prio[MaxM], siz[MaxM], cnt[MaxM];
int son[MaxM][2], data[MaxM];
void pushUp(int o){
rep(j,siz[o]=0,1)
siz[o] += siz[son[o][j]];
siz[o] += cnt[o];
}
int merge(int a,int b){
if(!a || !b) return a^b;
if(prio[a] > prio[b]){
son[a][1] = merge(son[a][1],b);
pushUp(a); return a;
}
else{
son[b][0] = merge(a,son[b][0]);
pushUp(b); return b;
}
}
PII split(int o,unsigned k){
if(!o) return PII(o,o);
int d = (k > siz[son[o][0]]);
if(d) k -= siz[son[o][0]]+cnt[o];
PII p = split(son[o][d],k);
son[o][d] = p[d^1], p[d^1] = o;
pushUp(o); return p;
}
int cntNode;
int newNode(int D){
int &o = ++ cntNode;
son[o][0] = son[o][1] = 0;
prio[o] = rander();
cnt[o] = 1; data[o] = D;
pushUp(o); return o;
}
int rt;
unsigned getRank(int x){
int o = rt; unsigned res = 1;
for(int d; o; o=son[o][d]){
d = (data[o] <= x);
res += (siz[son[o][0]]+cnt[o])*d;
if(data[o] == x)
return res-cnt[o];
}
return res;
}
int kthElement(unsigned k){
int o = rt; // start from root
for(int d=0; o; o=son[o][d],d=0){
if(k > siz[son[o][0]]){
k -= siz[son[o][0]], d = 1;
if(k <= cnt[o])
return data[o];
else k -= cnt[o];
}
}
return data[0]; // what's that?
}
void insert(int x){
unsigned l = getRank(x)-1;
unsigned r = getRank(x+1);
PII rp = split(rt,r-1);
PII lp = split(rp[0],l);
if(!lp[1]) lp[1] = newNode(x);
else ++ cnt[lp[1]], ++ siz[lp[1]];
rt = merge(merge(lp[0],lp[1]),rp[1]);
}
void erase(int x){
unsigned l = getRank(x)-1;
unsigned r = getRank(x+1);
PII rp = split(rt,r-1);
PII lp = split(rp[0],l);
-- cnt[lp[1]], -- siz[lp[1]];
if(!cnt[lp[1]]) lp[1] = 0;
rt = merge(merge(lp[0],lp[1]),rp[1]);
}
}
版本二点五:笛卡尔树
名字听着很高大上,其实 狗屁不是 很通俗的。
如果我们按照 key \text{key} key 递增的顺序插入每一个点,想想我们 insert \text{insert} insert 函数递归到最底层的时候,这个点在哪里?
对了,最右边!因为此时它是整棵树中最大的一个。它一定在最右边。
如果我们需要进行 rotate \text{rotate} rotate 旋转,将它旋转上去,显然,只会涉及最右边的一条链。
所以,我们用一个栈存储最右边的那一条链上面的点。显然,插入这个点之后,这个点一定是栈顶,所以我们可以放心的弹栈。
void init(int n){ // 插入1-n的所有key
vector<int> v; v.clear(); newTreap();
for(int i=1,o,k; i<=n; ++i){
o = newNode(i);
while(not v.empty()){
k = v.back();
if(prio[k] < prio[o])
pushUp(son[o][0] = k);
else{
son[k][1] = o; break;
}
v.pop_back();
}
if(v.empty()) root = o;
// 别忘了更新root
v.push_back(o);
}
while(not v.empty())
pushUp(v.back()), v.pop_back();
}
例题
板子
传送门 to luogu。就是板题普通平衡树
。
#include <cstdio>
#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;
inline int readint(){
int a = 0, f = 1; char c = getchar();
for(; c<'0' or c>'9'; c=getchar())
if(c == '-') f = -1;
for(; '0'<=c and c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) writeint(x/10);
putchar(x%10+'0');
}
const int N = 500000;
namespace Treap{
int prio[N], data[N];
int son[N][2], root;
unsigned size[N], cnt[N];
vector<int> pool;
void newTreap(){
for(int i=1; i<N; ++i)
pool.push_back(i);
root = cnt[0] = size[0] = prio[0] = 0;
}
int newNode(int x){
int id = pool.back();
pool.pop_back();
data[id] = x, size[id] = cnt[id] = 1;
son[id][0] = son[id][1] = 0, prio[id] = rand();
return id;
}
void deleteNode(int id){
pool.push_back(id);
}
void pushUp(int o){
size[o] = cnt[o]+size[son[o][0]]+size[son[o][1]];
}
void decrease(int o){
-- cnt[o], -- size[o];
}
void increase(int o){
++ cnt[o], ++ size[o];
}
pair<int,int> split(int bound,int o){
// (x <= bound) and (bound < x)
if(not o) return make_pair(o,o);
pair<int,int> p;
if(data[o] <= bound){
p = split(bound,son[o][1]);
son[o][1] = p.first;
p.first = o;
}
else{
p = split(bound,son[o][0]);
son[o][0] = p.second;
p.second = o;
}
pushUp(o); return p;
}
int merge(int a,int b){
if(not a or not b) return a+b;
if(prio[a] < prio[b]) son[b][0] = merge(a,son[b][0]);
else son[a][1] = merge(son[a][1],b);
pushUp(a), pushUp(b);
return prio[a] < prio[b] ? b : a;
}
void insert(int x){
pair<int,int> rPart = split(x,root);
pair<int,int> lPart = split(x-1,rPart.first);
if(lPart.second) increase(lPart.second);
else lPart.second = newNode(x);
root = merge(merge(lPart.first,lPart.second),rPart.second);
}
void erase(int x){
pair<int,int> rPart = split(x,root);
pair<int,int> lPart = split(x-1,rPart.first);
if(cnt[lPart.second] > 1) decrease(lPart.second);
else deleteNode(lPart.second), lPart.second = 0;
root = merge(merge(lPart.first,lPart.second),rPart.second);
}
unsigned getRank(int x){
// 前驱的排名;(前驱<=x)
unsigned res = 0;
for(int o=root,d; o; o=son[o][d]){
d = data[o] <= x;
res += d*(cnt[o]+size[son[o][0]]);
if(data[o] == x){ res += 1-cnt[o]; break; }
}
return res;
}
int kthElement(unsigned x){ int o;
for(o=root; o; ){
if(x > size[son[o][0]]){
x -= size[son[o][0]];
if(x <= cnt[o]) break;
x -= cnt[o], o = son[o][1];
}else o = son[o][0];
}
return data[o];
}
unsigned count(int x){ int o;
for(o=root; o; o=son[o][data[o]<x])
if(data[o] == x) break;
return cnt[o];
}
}using namespace Treap;
int main(){
srand(5201314); newTreap();
for(int T=readint(); T; --T){
int opt = readint(), x = readint();
switch(opt){
case 1:insert(x); break;
case 2:erase(x); break;
case 3:writeint(getRank(x)); putchar('\n'); break;
case 4:writeint(kthElement(x)); putchar('\n'); break;
case 5:{
int ans = kthElement(getRank(x)-(count(x)?1:0));
writeint(ans); putchar('\n'); break;
}
case 6:{
int ans = kthElement(getRank(x)+max(count(x),1u));
writeint(ans); putchar('\n'); break;
}
default:puts("怎么可能?你在想peach"); break;
}
}
return 0;
}
书架
点此跳转。感觉不是很难 但是也不会。
文艺平衡树
电磁跳转。似乎不是很难?
后记
我两个版本都出现了同一个错误——
算排名的时候, data=x \text{data=x} data=x时,我直接 res++ \text{res++} res++就跑了。忘记了左子树的 size \text{size} size!
要不是这一点,我都不想写这该死的【学习笔记】博客