Treap
简介
Treap(树堆)是一种 弱平衡 的 二叉搜索树。
Treap 的结点除了被维护的 权值(
其中,二叉搜索树的性质是指:
- 左子节点的权值(
)比父节点小。 - 右子节点的权值(
)比父节点大。
堆的性质是:
- 子节点优先级(
)比父节点大或小(取决于是小根堆还是大根堆)。
不难看出,如果用的是同一个值,那么这两种数据结构在组合后会变成一条链,所以我们再在搜索树的基础上,引入一个给堆的值
下图就是一个 Treap 的例子(这里使用的是小根堆,即根节点的优先级最小)。
那我们为什么需要大费周章的去让这个数据结构符合树和堆的性质,并且随机给出堆的值呢?
要理解这个,首先需要理解朴素二叉搜索树的问题。在给朴素搜索树插入一个新节点时,我们需要从这个搜索树的根节点开始递归,如果新节点比当前节点小,那就向左递归,反之亦然。
最后当发现当前节点没有子节点时,就根据新节点的值的大小,让新节点成为当前节点的左或右子节点。
如果插入结点的权值是随机的(换言之,是随机插入的),那这个朴素搜索树的高度较小(接近
不过,这只是在随机情况下的复杂度,如果我们按照下面这个非常有序的顺序给一个朴素的搜索树插入节点:
那么这棵树将会退化成链,即变得非常「瘦长」(每次插入的节点都比前面的大,所以都被安排到右子节点了):
不难看出,查询的复杂度也从
而 treap 为了解决这个问题、达到一个较为「平衡」的状态,通过维护随机的优先级满足堆性质,「打乱」了节点的插入顺序,从而让二叉搜索树达到了理想的复杂度,避免了退化成链的问题。
Treap 复杂度的证明
由于 treap 各种操作的复杂度都和所操作的结点的深度有关,我们首先证明,所有结点的期望高度都是
记号约定
为了方便表述,我们约定:
是节点个数。 - Treap 结点中满足二叉搜索树性质的称为 权值,满足堆性质的(也就是随机的)称为 优先级。不妨设优先级满足小根堆性质。
表示权值第 小的结点。 表示集合 ,即按权值升序排列后第 个到第 个的结点构成的集合。 表示结点 的深度。规定根节点的深度是 . 是一个指示器随机变量,当 是 的祖先时值为 ,否则为 . 特别地, . 表示事件 发生的概率。
树高的证明
由于结点
那么根据期望的线性性,有
由于
我们先证明引理:
引理的证明
证明:考虑分类讨论
- 若
是根节点:由于优先级满足小根堆性质, 的优先级最小,并且对于任意的 , 都是 的祖先。 - 若
是根节点:同理, 优先级最小,因此 不是 中优先级最小的;同时 也不是 的祖先。 - 若
和 在根节点的两个子树中(一左一右),那么根节点 . 因此 的优先级不可能是 中最小的(因为根节点的比它小)。同时,由于 和 分属两个子树, 也不是 的祖先。 - 若
和 在根节点的同一个子树中,此时可以将这个子树单独拿出来作为一棵新的 treap,递归进行上面的证明即可。
那么根据引理,深度的期望可以转化成
又因为结点的优先级是随机的,我们假定集合
因此每个结点的期望高度都是
而朴素的二叉搜索树的操作的复杂度均是
期望复杂度的感性理解
首先,我们需要认识到一个节点的
- 子节点值(
)比父节点大或小(取决于是小根堆还是大根堆)
我们发现层数低的节点,比如整个树的根节点,它的
给 treap 插入新节点时,需要同时维护树和堆的性质。其中,搜索树的性质可以在插入时维护,而堆性质的维护则有两种处理方法,分别是旋转和分裂、合并。使用这两种方法的 treap 被分别称为 旋转 treap 和 无旋 treap。
旋转 treap
旋转 treap 维护平衡的方式为旋转,和 AVL 树的旋转操作类似,分为 左旋 和 右旋。即在满足二叉搜索树的条件下根据堆的优先级对 treap 进行平衡操作。
旋转 treap 在做普通平衡树题的时候,是所有平衡树中常数较小的。
下面的讲解中的代码用指针实现了旋转 treap,文末附有数组形式的完整实现。
Info
代码中的 rank
代表前面讲的优先级(
节点结构
struct Node {
Node *ch[2]; // 两个子节点的地址
int val, rank;
int rep_cnt; // 当前这个值(val)重复出现的次数
int siz; // 以当前节点为根的子树大小
Node(int val) : val(val), rep_cnt(1), siz(1) {
ch[0] = ch[1] = nullptr;
rank = rand();
// 注意初始化的时候,rank 是随机给出的
}
void upd_siz() {
// 用于旋转和删除过后,重新计算 siz 的值
siz = rep_cnt;
if (ch[0] != nullptr) siz += ch[0]->siz;
if (ch[1] != nullptr) siz += ch[1]->siz;
}
};
旋转
旋转操作是 treap 的一个非常重要的操作,主要用来在保持 treap 树性质的同时,调整不同节点的层数,以达到维护堆性质的作用。
旋转操作的左旋和右旋可能不是特别容易区分,以下是两个较为明显的特点:
旋转操作的含义:
- 在不影响搜索树性质的前提下,把和旋转方向相反的子树变成根节点(如左旋,就是把右子树变成根节点)
- 不影响性质,并且在旋转过后,跟旋转方向相同的子节点变成了原来的根节点(如左旋,旋转完之后的左子节点是旋转前的根节点)
左旋和右旋操作是相互的,如下图。
enum rot_type { LF = 1, RT = 0 };
void _rotate(Node *&cur,
rot_type dir) { // dir参数代表旋转的方向 0为右旋,1为左旋
// 注意传进来的 cur 是指针的引用,也就是改了这个
// cur,变量是跟着一起改的,如果这个 cur 是别的 树的子节点,根据 ch
// 找过来的时候,也是会找到这里的
// 以下的代码解释的均是左旋时的情况
Node *tmp = cur->ch[dir]; // 让 C 变成根节点,
// 这里的 tmp
// 是一个临时的节点指针,指向成为新的根节点的节点
/* 左旋:也就是让右子节点变成根节点
* A C
* / \ / \
* B C ----> A E
* / \ / \
* D E B D
*/
cur->ch[dir] = tmp->ch[!dir]; // 让 A 的右子节点变成 D
tmp->ch[!dir] = cur; // 让 C 的左子节点变成 A
cur->upd_siz(), tmp->upd_siz(); // 更新大小信息
cur = tmp; // 最后把临时储存 C 树的变量赋值到当前根节点上(注意 cur 是引用)
}
插入
类似普通二叉搜索树的插入,但是需要在插入的过程中通过旋转来维护优先级的堆性质。
void _insert(Node *&cur, int val) {
if (cur == nullptr) {
// 没这个节点直接新建
cur = new Node(val);
return;
} else if (val == cur->val) {
// 如果有这个值相同的节点,就把重复数量加一
cur->rep_cnt++;
cur->siz++;
} else if (val < cur->val) {
// 维护搜索树性质,val 比当前节点小就插到左边,反之亦然
_insert(cur->ch[0], val);
if (cur->ch[0]->rank < cur->rank) {
// 小根堆中,上面节点的优先级一定更小
// 因为新插的左子节点比父节点小,现在需要让左子节点变成父节点
_rotate(cur, RT); // 注意前面的旋转性质,要把左子节点转上来,需要右旋
}
cur->upd_siz(); // 插入之后大小会变化,需要更新
} else {
_insert(cur->ch[1], val);
if (cur->ch[1]->rank < cur->rank) {
_rotate(cur, LF);
}
cur->upd_siz();
}
}
删除
主要就是分类讨论,不同的情况有不同的处理方法,删完了树的大小会有变化,要注意更新。并且如果要删的节点有左子树和右子树,就要考虑删除之后让谁来当父节点(维护 rank 小的节点在上面)。
void _del(Node *&cur, int val) {
if (val > cur->val) {
_del(cur->ch[1], val);
// 值更大就在右子树,反之亦然
cur->upd_siz();
} else if (val < cur->val) {
_del(cur->ch[0], val);
cur->upd_siz();
} else {
if (cur->rep_cnt > 1) {
// 如果要删除的节点是重复的,可以直接把重复值减小
cur->rep_cnt--, cur->siz--;
return;
}
uint8_t state = 0;
state |= (cur->ch[0] != nullptr);
state |= ((cur->ch[1] != nullptr) << 1);
// 00都无,01有左无右,10,无左有右,11都有
Node *tmp = cur;
switch (state) {
case 0:
delete cur;
cur = nullptr;
// 没有任何子节点,就直接把这个节点删了
break;
case 1: // 有左无右
cur = tmp->ch[0];
// 把根变成左儿子,然后把原来的根节删了,注意这里的 tmp 是从 cur
// 复制的,而 cur 是引用
delete tmp;
break;
case 2: // 有右无左
cur = tmp->ch[1];
delete tmp;
break;
case 3:
rot_type dir = cur->ch[0]->rank < cur->ch[1]->rank
? RT
: LF; // dir 是 rank 更小的那个儿子
_rotate(cur, dir); // 这里的旋转可以把优先级更小的儿子转上去,rt 是 0,
// 而 lf 是 1,刚好跟实际的子树下标反过来
_del(
cur->ch[!dir],
val); // 旋转完成后原来的根节点就在旋方向那边,所以需要
// 继续把这个原来的根节点删掉
// 如果说要删的这个节点是在整个树的「上层的」,那我们会一直通过这
// 这里的旋转操作,把它转到没有子树了(或者只有一个),再删掉它。
cur->upd_siz();
// 删除会造成大小改变
break;
}
}
}
根据值查询排名
操作含义:查询以 cur 为根节点的子树中,val 这个值的大小的排名(该子树中小于 val 的节点的个数 + 1)
int _query_rank(Node *cur, int val) {
int less_siz = cur->ch[0] == nullptr ? 0 : cur->ch[0]->siz;
// 这个树中小于 val 的节点的数量
if (val == cur->val)
// 如果这个节点就是要查的节点
return less_siz + 1;
else if (val < cur->val) {
if (cur->ch[0] != nullptr)
return _query_rank(cur->ch[0], val);
else
return 1; // 如果左子树是空的,说比最小的节点还要小,那这个数字就是最小的
} else {
if (cur->ch[1] != nullptr)
// 如果要查的值比这个节点大,那这个节点的左子树以及这个节点自身肯定都比要查的值小
// 所以要加上这两个值,再加上往右边找的结果
// (以右子树为根的子树中,val 这个值的大小的排名)
return less_siz + cur->rep_cnt + _query_rank(cur->ch[1], val);
else
return cur->siz + 1;
// 没有右子树的话直接整个树 + 1 相当于 less_siz + cur->rep_cnt + 1
}
}
根据排名查询值
要根据排名查询值,我们首先要知道如何判断要查的节点在树的哪个部分:
以下是一个判断方法的表:
左子树 | 根节点/当前节点 | 右子树 |
---|---|---|
排名 ≤ 左子树的大小 | 排名 > 左子树的大小,并且 ≤ 左子树的大小 + 根节点的重复次数 | 排名 > 左子树的大小 + 根节点的重复次数 |
注意如果在右子树,递归的时候需要对原来的 rank
进行处理。递归的时候就相当去查,在右子树中为这个排名的值,为了把排名转换成基于右子树的,需要把原来的 rank
减去左子树的大小和根节点的重复次数。
可以把所有节点想象成一个排好序的数组,或者数轴(如下),
1 -> |左子树的节点|根节点|右子树的节点| -> n
^
要查的排名
⬇转换成基于右子树的排名
1 -> |右子树的节点| -> n
^
要查的排名
这里的转换方法就是直接把排名减去左子树的大小和根节点的重复数量。
int _query_val(Node *cur, int rank) {
// 查询树中第 rank 大的节点的值
int less_siz = cur->ch[0] == nullptr ? 0 : cur->ch[0]->siz;
// less siz 是左子树的大小
if (rank <= less_siz)
return _query_val(cur->ch[0], rank);
else if (rank <= less_siz + cur->rep_cnt)
return cur->val;
else
return _query_val(cur->ch[1], rank - less_siz - cur->rep_cnt); // 见前文
}
查询第一个比 val 小的节点
注意这里使用了一个类中的全局变量,q_prev_tmp
。
这个值是只有在 val 比当前节点值大的时候才会被更改的,所以返回这个变量就是返回 val 最后一次比当前节点的值大,之后就是更小了。
int _query_prev(Node *cur, int val) {
if (val <= cur->val) {
// 还是比 val 大,所以往左子树找
if (cur->ch[0] != nullptr) return _query_prev(cur->ch[0], val);
} else {
// 只有能进到这个 else 里,才会更新 q_prev_tmp 的值
q_prev_tmp = cur->val;
// 当前节点已经比 val,小了,但是不确定是否是最大的,所以要到右子树继续找
if (cur->ch[1] != nullptr) _query_prev(cur->ch[1], val);
// 接下来的递归可能不会更改 q_prev_tmp
// 了,那就直接返回这个值,总之返回的就是最后一次进到 这个 else 中的
// cur->val
return q_prev_tmp;
}
return NIL;
}
查询第一个比 val 大的节点
跟前一个很相似,只是大于小于号换了一下。
int _query_nex(Node *cur, int val) {
if (val >= cur->val) {
if (cur->ch[1] != nullptr) return _query_nex(cur->ch[1], val);
} else {
q_nex_tmp = cur->val;
if (cur->ch[0] != nullptr) _query_nex(cur->ch[0], val);
return q_nex_tmp;
}
return NIL;
}
无旋 treap
无旋 treap 的操作方式使得它天生支持维护序列、可持久化等特性。
无旋 treap 又称分裂合并 treap。它仅有两种核心操作,即为 分裂 与 合并。通过这两种操作,在很多情况下可以比旋转 treap 更方便的实现别的操作。下面逐一介绍这两种操作。
注释
讲解无旋 treap 应当提到 FHQ-Treap(by 范浩强)。即可持久化,支持区间操作的无旋 Treap。更多内容请参照《范浩强谈数据结构》ppt。
分裂(split)
按值分裂
分裂过程接受两个参数:根指针
该过程首先判断
相应的,如果
下图展示了
pair<Node *, Node *> split(Node *cur, int key) {
if (cur == nullptr) return {nullptr, nullptr};
if (cur->val <= key) {
// cur 以及它的左子树一定属于分裂后的第一个树
auto temp = split(cur->ch[1], key);
// 但是它可能有部分右子树也比 key 小
cur->ch[1] = temp.first;
// 我们把小于 key 的那部分拿出来,作为 cur 的右子树,这样整个 cur 都是小于
// key 的 剩下的那部分右子树成为分裂后的第二个 treap
cur->upd_siz();
// 分裂过后树的大小会变化,需要更新
return {cur, temp.second};
} else {
// 同上
auto temp = split(cur->ch[0], key);
cur->ch[0] = temp.second;
cur->upd_siz();
return {temp.first, cur};
}
}
按排名分裂
比起按值分裂,这个操作更像是旋转 treap 中的根据排名(某个节点的排名是树中所有小于此节点值的节点的数量
此函数接受两个参数,节点指针
其中,第一个 treap 中每个节点的排名都小于 Node
结构体中的 cnt
),第三个则是大于。
此操作的重点在于判断排名和
并且,此操作的递归部分和按值分裂也非常相似,这里不赘述。
tuple<Node *, Node *, Node *> split_by_rk(Node *cur, int rk) {
if (cur == nullptr) return {nullptr, nullptr, nullptr};
int ls_siz = cur->ch[0] == nullptr ? 0 : cur->ch[0]->siz;
if (rk <= ls_siz) {
// 排名和 cur 相等的节点在左子树
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(cur->ch[0], rk);
cur->ch[0] = r; // 返回的第三个 treap 中的排名都大于 rk
// cur 的左子树被设成 r 后,整个 cur 中节点的排名都大于 rk
cur->upd_siz();
return {l, mid, cur};
} else if (rk <= ls_siz + cur->cnt) {
// 和 cur 相等的就是当前节点
Node *lt = cur->ch[0];
Node *rt = cur->ch[1];
cur->ch[0] = cur->ch[1] = nullptr;
// 分裂后第二个 treap 只有一个节点,所有要把它的子树设置为空
return {lt, cur, rt};
} else {
// 排名和 cur 相等的节点在右子树
// 递归过程同上
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(cur->ch[1], rk - ls_siz - cur->cnt);
cur->ch[1] = l;
cur->upd_siz();
return {cur, mid, r};
}
}
合并(merge)
合并过程接受两个参数:左 treap 的根指针
在旋转 treap 中,我们借助旋转操作来维护
因为两个 treap 已经有序,所以我们在合并的时候只需要考虑把哪个树「放在上面」,把哪个「放在下面」,也就是是需要判断将哪个一个树作为子树。显然,根据堆的性质,我们需要把
同时,我们还需要满足搜索树的性质,所以若
Node *merge(Node *u, Node *v) {
// 传进来的两个树的内部已经符合搜索树的性质了
// 并且 u 内所有节点的值 < v 内所有节点的值
// 所以在合并的时候需要维护堆的性质
// 这里用的是小根堆
if (u == nullptr && v == nullptr) return nullptr;
if (u != nullptr && v == nullptr) return u;
if (v != nullptr && u == nullptr) return v;
if (u->prio < v->prio) {
// u 的 prio 比较小,u应该作为父节点
u->ch[1] = merge(u->ch[1], v);
// 因为 v 比 u 大,所以把 v 作为 u 的右子树
u->upd_siz();
return u;
} else {
// v 比较小,v应该作为父节点
v->ch[0] = merge(u, v->ch[0]);
// u 比 v 小,所以递归时的参数是这样的
v->upd_siz();
return v;
}
}
插入
在无旋 treap 中,插入,删除,根据值查询排名等基础操作既可以用普通二叉查找树的方法实现,也可以用分裂和合并来实现。通常来说,使用分裂和合并来实现更加简洁,但是速度会慢一点2。为了帮助更好的理解无旋 treap,下面的操作全部使用分裂和合并实现。
在实现插入操作时,我们利用了分裂操作的一些性质。也就是值小于等于
所以,假设我们根据
其中
如果我们再按照
其中
不难发现,只要
在插入时,如果我们发现符合
注意把树分裂好了还需要用合并操作把它「粘」回去,这样下次还能继续使用。并且,还需要注意合并操作的参数顺序是有要求的,第一个树的所有节点的值都需要小于第二个。
void insert(int val) {
auto temp = split(root, val);
// 根据 val 的值把整个树分成两个
// 注意 split 的实现,等于 val 的子树是在左子树的
auto l_tr = split(temp.first, val - 1);
// l_tr 的左子树 <= val - 1,如果有 = val 的节点,那一定在右子树
Node *new_node;
if (l_tr.second == nullptr) {
// 没有这个节点就新开,否则直接增加重复次数。
new_node = new Node(val);
} else {
l_tr.second->cnt++;
l_tr.second->upd_siz();
}
Node *l_tr_combined =
merge(l_tr.first, l_tr.second == nullptr ? new_node : l_tr.second);
// 合并 T_1 left 和 T_1 right
root = merge(l_tr_combined, temp.second);
// 合并 T_1 和 T_2
}
删除
删除操作也使用和插入操作相似的方法,找到值和
void del(int val) {
auto temp = split(root, val);
auto l_tr = split(temp.first, val - 1);
if (l_tr.second->cnt > 1) {
// 如果这个节点的重复次数大于 1,减小即可
l_tr.second->cnt--;
l_tr.second->upd_siz();
l_tr.first = merge(l_tr.first, l_tr.second);
} else {
if (temp.first == l_tr.second) {
// 有可能整个 T_1 只有这个节点,所以也需要把这个点设成 null 来标注已经删除
temp.first = nullptr;
}
delete l_tr.second;
l_tr.second = nullptr;
}
root = merge(l_tr.first, temp.second);
}
根据值查询排名
排名是比这个值小的节点的数量
如果树的值和
int qrank_by_val(Node* cur, int val) {
auto temp = split(cur, val - 1);
int ret = (temp.first == nullptr ? 0 : temp.first->siz) + 1; // 根据定义 + 1
root = merge(temp.first, temp.second); // 拆好了再粘回去
return ret;
}
根据排名查询值
调用 split_by_rk()
函数后,会返回分裂好的三个 treap,其中第二个只包含一个节点,它的排名等于
int qval_by_rank(Node *cur, int rk) {
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(cur, rk);
int ret = mid->val;
root = merge(merge(l, mid), r);
return ret;
}
查询第一个比 val 小的节点
可以把这个问题转化为,在比 qval_by_rank()
找出这个树中值最大的节点。
int qprev(int val) {
auto temp = split(root, val - 1);
// temp.first 就是值小于 val 的子树
int ret = qval_by_rank(temp.first, temp.first->siz);
// 这里查询的是,所有小于 val 的节点里面,最大的那个的值
root = merge(temp.first, temp.second);
return ret;
}
查询第一个比 val 大的节点
和上个操作类似,可以把这个问题转化为,在比
然后我们去查询这个树中排名为
int qnex(int val) {
auto temp = split(root, val);
int ret = qval_by_rank(temp.second, 1);
// 查询所有大于 val 的子树里面,值最小的那个
root = merge(temp.first, temp.second);
return ret;
}
建树(build)
将一个有
可以依次暴力插入这
在某些题目内,可能会有多次插入一段有序序列的操作,这是就需要在
方法一:在递归建树的过程中,每次选取当前区间的中点作为该区间的树根,并对每个节点钦定合适的优先值,使得新树满足堆的性质。这样能保证树高为
方法二:在递归建树的过程中,每次选取当前区间的中点作为该区间的树根,然后给每个节点一个随机优先级。这样能保证树高为 merge
操作更加随机一点,而不是用来保证树高的。
方法三:观察到 treap 是笛卡尔树,利用笛卡尔树的
无旋 treap 的区间操作
建树
无旋 treap 相比旋转 treap 的一大好处就是可以实现各种区间操作,下面我们以文艺平衡树的 模板题 为例,介绍 treap 的区间操作。
您需要写一种数据结构(可参考题目标题),来维护一个有序数列。
其中需要提供以下操作:翻转一个区间,例如原有序序列是
,翻转区间是 的话,结果是 。 对于 的数据, (初始区间长度) (翻转次数)
在这道题目中,我们需要实现的是区间翻转,那么我们首先需要考虑如何建树,建出来的树需要是初始的区间。
我们只需要把区间的下标依次插入 treap 中,这样在中序遍历(先遍历左子树,然后当前节点,最后右子树)时,就可以得到这个区间3。
我们知道在朴素的二叉查找树中按照递增的顺序插入节点,建出来的树是一个长链,按照中序遍历,自然可以得到这个区间。
如上图,按照
但是在 treap 中,按增序插入节点后,在合并操作时还会根据
可以参考 笛卡尔树的单调栈建树方法 来理解这个问题。
设新插入的节点为
首先,因为是递增地插入节点,每一个新插入的节点肯定会被连接到 treap 的右链(即从根结点一直往右子树走,经过的结点形成的链)上。
从根节点开始,右链上的节点的
因为
可以发现,中序遍历时
下图是一个 treap 根据递增顺序插入
区间翻转
翻转
翻转的具体操作是把区间内的子树的每一个左,右子节点交换位置。如下图就展示了翻转上图中 treap 的
注意如果按照这个方法翻转,那么每次翻转
再观察题目要求,可以发现因为只需要最后输出操作完的区间,所以并不需要每次都真的去交换。如此一来,便可以使用线段树中常用的懒标记(lazy tag)来优化复杂度。交换时,只需要在父节点打上标记,代表这个子树下的每个左右子节点都需要交换就行了。
在线段树中,我们一般在更新和查询时下传懒标记。这是因为,在更新和查询时,我们想要更新/查询的范围不一定和懒标记代表的范围重合,所以要先下传标记,确保查到和更新后的值是正确的。
在无旋 treap 中也是一样。具体操作时我们会把 treap 分裂成前文讲到的三个树,然后给中间的树打上懒标记后合并这三棵树。因为我们想要翻转的区间和懒标记代表的区间不一定重合,所以要在分裂时下传标记。并且,分裂和合并操作会造成每个节点及其懒标记所代表的节点发生变动,所以也需要在合并前下传懒标记。
换句话说,是当树的结构发生改变的时候,当我们进行分裂或合并操作时需要改变某一个点的左右儿子信息时之前,应该下放标记,而非之后,因为懒标记是需要下传给儿子节点的,但更改左右儿子信息之后若懒标记还未下放,则懒标记就丢失了下放的对象。4
以下为代码讲解,代码参考了3。
因为区间操作中大部分操作都和普通的无旋 treap 相同,所以这里只讲解和普通无旋 treap 不同的地方。
下传标记
需要注意这里的懒标记代表需要把这个树中的每一个子节点交换位置。所以如果当前节点的子节点也有懒标记,那两次翻转就抵消了。如果子节点不需要翻转,那么这个懒标记就需要继续被下传到子节点上。
// 这里这个 pushdown 是 Node 类的成员函数,其中 to_rev 是懒标记
void pushdown() {
swap(ch[0], ch[1]);
if (ch[0] != nullptr) ch[0]->to_rev ^= 1;
if (ch[1] != nullptr) ch[1]->to_rev ^= 1;
to_rev = false;
}
void check_tag() {
if (to_rev) pushdown();
}
分裂
注意在这个题目中,因为翻转操作,treap 中的
所以这里的分裂跟普通无旋 treap 中的按排名分裂更相似,是根据当前树的大小判断往左还是右子树递归的,换言之,我们是按照开始时这个节点在树中的位置来判断的。
返回的第一个 treap 中节点的排名全部小于等于
#define siz(_) (_ == nullptr ? 0 : _->siz)
pair<Node*, Node*> split(Node* cur, int sz) {
// 按照树的大小判断
if (cur == nullptr) return {nullptr, nullptr};
cur->check_tag();
// 分裂前先下传
if (sz <= siz(cur->ch[0])) {
auto temp = split(cur->ch[0], sz);
cur->ch[0] = temp.second;
cur->upd_siz();
return {temp.first, cur};
} else {
auto temp =
split(cur->ch[1],
sz - siz(cur->ch[0]) -
1); // 这里的转换在有旋 treap 的 「根据排名查询值有讲」
cur->ch[1] = temp.first;
cur->upd_siz();
return {cur, temp.second};
}
}
合并
唯一需要注意的是在合并前下传懒标记
Node *merge(Node *sm, Node *bg) {
// small, big
if (sm == nullptr && bg == nullptr) return nullptr;
if (sm != nullptr && bg == nullptr) return sm;
if (sm == nullptr && bg != nullptr) return bg;
sm->check_tag(), bg->check_tag();
if (sm->prio < bg->prio) {
sm->ch[1] = merge(sm->ch[1], bg);
sm->upd_siz();
return sm;
} else {
bg->ch[0] = merge(sm, bg->ch[0]);
bg->upd_siz();
return bg;
}
}
区间翻转
和前面介绍的一样,分裂出
void seg_rev(int l, int r) {
// 这里的 less 和 more 是相对于 l 的
auto less = split(root, l - 1);
// 所有小于等于 l - 1 的会在 less 的左子树
auto more = split(less.second, r - l + 1);
// 从 l 开始的前 r - l + 1 个元素的区间
more.first->to_rev = true;
root = merge(less.first, merge(more.first, more.second));
}
中序遍历打印
要注意在打印时要下传标记。
void print(Node* cur) {
if (cur == nullptr) return;
cur->check_tag();
// 中序遍历 -> 先左子树,再自己,最后右子树
print(cur->ch[0]);
cout << cur->val << " ";
print(cur->ch[1]);
}
完整代码
旋转 treap
指针实现
完整代码
以下是前文讲解的代码的完整版本,是普通平衡树的模板代码。
// author: (ttzytt)[ttzytt.com]
#include <cstdint>
#include <cstdio>
#include <cstdlib>
using namespace std;
struct Node {
Node *ch[2];
int val, rank;
int rep_cnt;
int siz;
Node(int val) : val(val), rep_cnt(1), siz(1) {
ch[0] = ch[1] = nullptr;
rank = rand();
}
void upd_siz() {
siz = rep_cnt;
if (ch[0] != nullptr) siz += ch[0]->siz;
if (ch[1] != nullptr) siz += ch[1]->siz;
}
};
class Treap {
private:
Node *root;
constexpr static int NIL = -1; // 用于表示查询的值不存在
enum rot_type { LF = 1, RT = 0 };
int q_prev_tmp = 0, q_nex_tmp = 0;
void _rotate(Node *&cur, rot_type dir) { // 0为右旋,1为左旋
Node *tmp = cur->ch[dir];
cur->ch[dir] = tmp->ch[!dir];
tmp->ch[!dir] = cur;
cur->upd_siz(), tmp->upd_siz();
cur = tmp;
}
void _insert(Node *&cur, int val) {
if (cur == nullptr) {
cur = new Node(val);
return;
} else if (val == cur->val) {
cur->rep_cnt++;
cur->siz++;
} else if (val < cur->val) {
_insert(cur->ch[0], val);
if (cur->ch[0]->rank < cur->rank) {
_rotate(cur, RT);
}
cur->upd_siz();
} else {
_insert(cur->ch[1], val);
if (cur->ch[1]->rank < cur->rank) {
_rotate(cur, LF);
}
cur->upd_siz();
}
}
void _del(Node *&cur, int val) {
if (val > cur->val) {
_del(cur->ch[1], val);
cur->upd_siz();
} else if (val < cur->val) {
_del(cur->ch[0], val);
cur->upd_siz();
} else {
if (cur->rep_cnt > 1) {
cur->rep_cnt--, cur->siz--;
return;
}
uint8_t state = 0;
state |= (cur->ch[0] != nullptr);
state |= ((cur->ch[1] != nullptr) << 1);
// 00都无,01有左无右,10,无左有右,11都有
Node *tmp = cur;
switch (state) {
case 0:
delete cur;
cur = nullptr;
break;
case 1: // 有左无右
cur = tmp->ch[0];
delete tmp;
break;
case 2: // 有右无左
cur = tmp->ch[1];
delete tmp;
break;
case 3:
rot_type dir = cur->ch[0]->rank < cur->ch[1]->rank ? RT : LF;
_rotate(cur, dir);
_del(cur->ch[!dir], val);
cur->upd_siz();
break;
}
}
}
int _query_rank(Node *cur, int val) {
int less_siz = cur->ch[0] == nullptr ? 0 : cur->ch[0]->siz;
if (val == cur->val)
return less_siz + 1;
else if (val < cur->val) {
if (cur->ch[0] != nullptr)
return _query_rank(cur->ch[0], val);
else
return 1;
} else {
if (cur->ch[1] != nullptr)
return less_siz + cur->rep_cnt + _query_rank(cur->ch[1], val);
else
return cur->siz + 1;
}
}
int _query_val(Node *cur, int rank) {
int less_siz = cur->ch[0] == nullptr ? 0 : cur->ch[0]->siz;
if (rank <= less_siz)
return _query_val(cur->ch[0], rank);
else if (rank <= less_siz + cur->rep_cnt)
return cur->val;
else
return _query_val(cur->ch[1], rank - less_siz - cur->rep_cnt);
}
int _query_prev(Node *cur, int val) {
if (val <= cur->val) {
if (cur->ch[0] != nullptr) return _query_prev(cur->ch[0], val);
} else {
q_prev_tmp = cur->val;
if (cur->ch[1] != nullptr) _query_prev(cur->ch[1], val);
return q_prev_tmp;
}
return NIL;
}
int _query_nex(Node *cur, int val) {
if (val >= cur->val) {
if (cur->ch[1] != nullptr) return _query_nex(cur->ch[1], val);
} else {
q_nex_tmp = cur->val;
if (cur->ch[0] != nullptr) _query_nex(cur->ch[0], val);
return q_nex_tmp;
}
return NIL;
}
public:
void insert(int val) { _insert(root, val); }
void del(int val) { _del(root, val); }
int query_rank(int val) { return _query_rank(root, val); }
int query_val(int rank) { return _query_val(root, rank); }
int query_prev(int val) { return _query_prev(root, val); }
int query_nex(int val) { return _query_nex(root, val); }
};
Treap tr;
int main() {
srand(0);
int t;
scanf("%d", &t);
while (t--) {
int mode;
int num;
scanf("%d%d", &mode, &num);
switch (mode) {
case 1:
tr.insert(num);
break;
case 2:
tr.del(num);
break;
case 3:
printf("%d\n", tr.query_rank(num));
break;
case 4:
printf("%d\n", tr.query_val(num));
break;
case 5:
printf("%d\n", tr.query_prev(num));
break;
case 6:
printf("%d\n", tr.query_nex(num));
break;
}
}
}
数组实现
以下是 bzoj 普通平衡树模板代码,使用数组实现。
完整代码
#include <iostream>
constexpr int MAXN = 100005;
constexpr int INF = 1 << 30;
int n;
struct treap { // 直接维护成数据结构,可以直接用
int l[MAXN], r[MAXN], val[MAXN], rnd[MAXN], size_[MAXN], w[MAXN];
int sz, ans, rt;
void pushup(int x) { size_[x] = size_[l[x]] + size_[r[x]] + w[x]; }
void lrotate(int &k) {
int t = r[k];
r[k] = l[t];
l[t] = k;
size_[t] = size_[k];
pushup(k);
k = t;
}
void rrotate(int &k) {
int t = l[k];
l[k] = r[t];
r[t] = k;
size_[t] = size_[k];
pushup(k);
k = t;
}
void insert(int &k, int x) { // 插入
if (!k) {
sz++;
k = sz;
size_[k] = 1;
w[k] = 1;
val[k] = x;
rnd[k] = rand();
return;
}
size_[k]++;
if (val[k] == x) {
w[k]++;
} else if (val[k] < x) {
insert(r[k], x);
if (rnd[r[k]] < rnd[k]) lrotate(k);
} else {
insert(l[k], x);
if (rnd[l[k]] < rnd[k]) rrotate(k);
}
}
bool del(int &k, int x) { // 删除节点
if (!k) return false;
if (val[k] == x) {
if (w[k] > 1) {
w[k]--;
size_[k]--;
return true;
}
if (l[k] == 0 || r[k] == 0) {
k = l[k] + r[k];
return true;
} else if (rnd[l[k]] < rnd[r[k]]) {
rrotate(k);
return del(k, x);
} else {
lrotate(k);
return del(k, x);
}
} else if (val[k] < x) {
bool succ = del(r[k], x);
if (succ) size_[k]--;
return succ;
} else {
bool succ = del(l[k], x);
if (succ) size_[k]--;
return succ;
}
}
int queryrank(int k, int x) {
if (!k) return 0;
if (val[k] == x)
return size_[l[k]] + 1;
else if (x > val[k]) {
return size_[l[k]] + w[k] + queryrank(r[k], x);
} else
return queryrank(l[k], x);
}
int querynum(int k, int x) {
if (!k) return 0;
if (x <= size_[l[k]])
return querynum(l[k], x);
else if (x > size_[l[k]] + w[k])
return querynum(r[k], x - size_[l[k]] - w[k]);
else
return val[k];
}
void querypre(int k, int x) {
if (!k) return;
if (val[k] < x)
ans = k, querypre(r[k], x);
else
querypre(l[k], x);
}
void querysub(int k, int x) {
if (!k) return;
if (val[k] > x)
ans = k, querysub(l[k], x);
else
querysub(r[k], x);
}
} T;
using std::cin;
using std::cout;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
srand(123);
cin >> n;
int opt, x;
for (int i = 1; i <= n; i++) {
cin >> opt >> x;
if (opt == 1)
T.insert(T.rt, x);
else if (opt == 2)
T.del(T.rt, x);
else if (opt == 3) {
cout << T.queryrank(T.rt, x) << '\n';
} else if (opt == 4) {
cout << T.querynum(T.rt, x) << '\n';
} else if (opt == 5) {
T.ans = 0;
T.querypre(T.rt, x);
cout << T.val[T.ans] << '\n';
} else if (opt == 6) {
T.ans = 0;
T.querysub(T.rt, x);
cout << T.val[T.ans] << '\n';
}
}
return 0;
}
无旋 treap
指针实现
完整代码
以下是前文讲解的代码的完整版本,是普通平衡树的模板代码。
// author: (ttzytt)[ttzytt.com]
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <tuple>
using namespace std;
struct Node {
Node *ch[2];
int val, prio;
int cnt;
int siz;
Node(int _val) : val(_val), cnt(1), siz(1) {
ch[0] = ch[1] = nullptr;
prio = rand();
}
Node(Node *_node) {
val = _node->val, prio = _node->prio, cnt = _node->cnt, siz = _node->siz;
}
void upd_siz() {
siz = cnt;
if (ch[0] != nullptr) siz += ch[0]->siz;
if (ch[1] != nullptr) siz += ch[1]->siz;
}
};
struct none_rot_treap {
#define _3 second.second
#define _2 second.first
Node *root;
pair<Node *, Node *> split(Node *cur, int key) {
if (cur == nullptr) return {nullptr, nullptr};
if (cur->val <= key) {
auto temp = split(cur->ch[1], key);
cur->ch[1] = temp.first;
cur->upd_siz();
return {cur, temp.second};
} else {
auto temp = split(cur->ch[0], key);
cur->ch[0] = temp.second;
cur->upd_siz();
return {temp.first, cur};
}
}
tuple<Node *, Node *, Node *> split_by_rk(Node *cur, int rk) {
if (cur == nullptr) return {nullptr, nullptr, nullptr};
int ls_siz = cur->ch[0] == nullptr ? 0 : cur->ch[0]->siz;
if (rk <= ls_siz) {
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(cur->ch[0], rk);
cur->ch[0] = r;
cur->upd_siz();
return {l, mid, cur};
} else if (rk <= ls_siz + cur->cnt) {
Node *lt = cur->ch[0];
Node *rt = cur->ch[1];
cur->ch[0] = cur->ch[1] = nullptr;
return {lt, cur, rt};
} else {
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(cur->ch[1], rk - ls_siz - cur->cnt);
cur->ch[1] = l;
cur->upd_siz();
return {cur, mid, r};
}
}
Node *merge(Node *u, Node *v) {
if (u == nullptr && v == nullptr) return nullptr;
if (u != nullptr && v == nullptr) return u;
if (v != nullptr && u == nullptr) return v;
if (u->prio < v->prio) {
u->ch[1] = merge(u->ch[1], v);
u->upd_siz();
return u;
} else {
v->ch[0] = merge(u, v->ch[0]);
v->upd_siz();
return v;
}
}
void insert(int val) {
auto temp = split(root, val);
auto l_tr = split(temp.first, val - 1);
Node *new_node;
if (l_tr.second == nullptr) {
new_node = new Node(val);
} else {
l_tr.second->cnt++;
l_tr.second->upd_siz();
}
Node *l_tr_combined =
merge(l_tr.first, l_tr.second == nullptr ? new_node : l_tr.second);
root = merge(l_tr_combined, temp.second);
}
void del(int val) {
auto temp = split(root, val);
auto l_tr = split(temp.first, val - 1);
if (l_tr.second->cnt > 1) {
l_tr.second->cnt--;
l_tr.second->upd_siz();
l_tr.first = merge(l_tr.first, l_tr.second);
} else {
if (temp.first == l_tr.second) {
temp.first = nullptr;
}
delete l_tr.second;
l_tr.second = nullptr;
}
root = merge(l_tr.first, temp.second);
}
int qrank_by_val(Node *cur, int val) {
auto temp = split(cur, val - 1);
int ret = (temp.first == nullptr ? 0 : temp.first->siz) + 1;
root = merge(temp.first, temp.second);
return ret;
}
int qval_by_rank(Node *cur, int rk) {
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(cur, rk);
int ret = mid->val;
root = merge(merge(l, mid), r);
return ret;
}
int qprev(int val) {
auto temp = split(root, val - 1);
int ret = qval_by_rank(temp.first, temp.first->siz);
root = merge(temp.first, temp.second);
return ret;
}
int qnex(int val) {
auto temp = split(root, val);
int ret = qval_by_rank(temp.second, 1);
root = merge(temp.first, temp.second);
return ret;
}
};
none_rot_treap tr;
int main() {
srand(time(nullptr));
int t;
scanf("%d", &t);
while (t--) {
int mode;
int num;
scanf("%d%d", &mode, &num);
switch (mode) {
case 1:
tr.insert(num);
break;
case 2:
tr.del(num);
break;
case 3:
printf("%d\n", tr.qrank_by_val(tr.root, num));
break;
case 4:
printf("%d\n", tr.qval_by_rank(tr.root, num));
break;
case 5:
printf("%d\n", tr.qprev(num));
break;
case 6:
printf("%d\n", tr.qnex(num));
break;
}
}
}
无旋 treap 的区间操作
指针实现
完整代码
以下是前文讲解的代码的完整版本,是文艺平衡树题目的模板代码。
// author: (ttzytt)[ttzytt.com]
#include <cstdlib>
#include <ctime>
#include <iostream>
using namespace std;
// 参考:https://www.cnblogs.com/Equinox-Flower/p/10785292.html
struct Node {
Node* ch[2];
int val, prio;
int cnt;
int siz;
bool to_rev = false; // 需要把这个子树下的每一个节点都翻转过来
Node(int _val) : val(_val), cnt(1), siz(1) {
ch[0] = ch[1] = nullptr;
prio = rand();
}
int upd_siz() {
siz = cnt;
if (ch[0] != nullptr) siz += ch[0]->siz;
if (ch[1] != nullptr) siz += ch[1]->siz;
return siz;
}
void pushdown() {
swap(ch[0], ch[1]);
if (ch[0] != nullptr) ch[0]->to_rev ^= 1;
// 如果原来子节点也要翻转,那两次翻转就抵消了,如果子节点不翻转,那这个
// tag 就需要继续被 push 到子节点上
if (ch[1] != nullptr) ch[1]->to_rev ^= 1;
to_rev = false;
}
void check_tag() {
if (to_rev) pushdown();
}
};
struct Seg_treap {
Node* root;
#define siz(_) (_ == nullptr ? 0 : _->siz)
pair<Node*, Node*> split(Node* cur, int sz) {
// 按照树的大小划分
if (cur == nullptr) return {nullptr, nullptr};
cur->check_tag();
if (sz <= siz(cur->ch[0])) {
// 左边的子树就够了
auto temp = split(cur->ch[0], sz);
// 左边的子树不一定全部需要,temp.second 是不需要的
cur->ch[0] = temp.second;
cur->upd_siz();
return {temp.first, cur};
} else {
// 左边的加上右边的一部分(当然也包括这个节点本身)
auto temp = split(cur->ch[1], sz - siz(cur->ch[0]) - 1);
cur->ch[1] = temp.first;
cur->upd_siz();
return {cur, temp.second};
}
}
Node* merge(Node* sm, Node* bg) {
// small, big
if (sm == nullptr && bg == nullptr) return nullptr;
if (sm != nullptr && bg == nullptr) return sm;
if (sm == nullptr && bg != nullptr) return bg;
sm->check_tag(), bg->check_tag();
if (sm->prio < bg->prio) {
sm->ch[1] = merge(sm->ch[1], bg);
sm->upd_siz();
return sm;
} else {
bg->ch[0] = merge(sm, bg->ch[0]);
bg->upd_siz();
return bg;
}
}
void insert(int val) {
auto temp = split(root, val);
auto l_tr = split(temp.first, val - 1);
Node* new_node;
if (l_tr.second == nullptr) new_node = new Node(val);
Node* l_tr_combined =
merge(l_tr.first, l_tr.second == nullptr ? new_node : l_tr.second);
root = merge(l_tr_combined, temp.second);
}
void seg_rev(int l, int r) {
// 这里的 less 和 more 是相对于 l 的
auto less = split(root, l - 1);
// 所有小于等于 l - 1 的会在 less 的左边
auto more = split(less.second, r - l + 1);
// 拿出从 l 开始的前 r - l + 1 个
more.first->to_rev = true;
root = merge(less.first, merge(more.first, more.second));
}
void print(Node* cur) {
if (cur == nullptr) return;
cur->check_tag();
print(cur->ch[0]);
cout << cur->val << " ";
print(cur->ch[1]);
}
};
Seg_treap tr;
int main() {
srand(time(nullptr));
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) tr.insert(i);
while (m--) {
int l, r;
cin >> l >> r;
tr.seg_rev(l, r);
}
tr.print(tr.root);
}
例题
参考资料与注释
创建日期: 2018年7月11日