treaplay 的原理及其运用 ——由treap、splay到treaplay的演变与比较
清华大学计算机系计53 陈秋昊 E-mail: haohao0924@126.com
[摘要]本文围绕一种新的数据结构平衡树treaplay展开。先从二叉排序树引入,从二叉排序树的退化引出平衡树,分析了treap和splay各自的优缺点,treaplay思路产生与形成过程,以noi2005维护数列为例说明treaplay的实现方法,与treap和splay作比较,明确各自适用范围以及在各个范围下的效率情况。
1.引言
在信息学竞赛中,数据结构一直是一个热点问题,而平衡树,则是数据结构中最为常考的一种。平衡树的种类有很多,主要分为两大类,一类是严格意义上的平衡树,就是可以始终保证树的最深深度在log2n的级别内,树的结构始终平衡的平衡树,比如红黑树、SBT、treap等,另一种就是splay,本身不能保证树的结构的平衡性,但是可以保证均摊效率在log2n的级别内。而treaplay属于第一种平衡树,却可以完成splay的基本上所有的功能。
2.二叉排序树(BST)
二叉排序树是一种有序二叉树,定义如下:
- 空树是一颗二叉排序树。
- 如果一颗二叉树根节点的左子树、右子树都是二叉排序树,并且左子树所有节点的排序关键字的值 小于 根节点的排序关键字的值 小于 右子树所有节点的排序关键字的值,那么,这颗二叉树是一颗二叉排序树。
不难发现,二叉排序树有一个非常好的性质:二叉排序树的中序遍历得到的排序关键字的顺序是升序的。这个性质非常实用,是我们使用并且不断改良二叉排序树的原因。
至于二叉排序树的各种操作,不是本文重点,不再赘述。
我们发现,在普通情况下,二叉排序树的深度是在log2n的级别内,但是在极端情况下有可能退化成一根链,这时候,就出现了各种平衡树算法来对二叉排序树进行改良,以保证其效率不会退化。
3.treap
treap是一个非常实用的平衡树算法,相比于二叉排序树,仅仅多维护了一个heap_key域,利用二叉树单旋不改变中序的性质,在保持二叉排序树结构不被破坏的前提下,通过单旋使得heap_key域满足二叉堆的性质。而通过随机化heap_key域的值来保证对于任何数据,树深度期望在log2n的级别内[1]。
但是,和其他严格的平衡树一样,普通的treap的树的结构是稳定的,无法随意变动,所以,在处理区间修改上有些棘手,尤其是区间翻转之类的无法拆分的区间操作。
4.splay
splay,准确来说,不是平衡树,应该叫伸展树。传统的splay是用单旋和双旋完成将一个节点提根的操作,而现在的splay是通过二重父亲与孩子的关系,判断旋转顺序,仅用单旋来完成提根操作。可以证明,splay的均摊复杂度是在log2n的级别内[2]。
因为有了提根操作,使得splay可以在保证效率不退化的前提下轻易改变二叉排序树的树结构,将一段区间旋转成为一颗子树,方便整体操作。
但是,splay有一个很大的问题:常数过大。一般来说,对于普通的平衡树操作,splay的常数大致是普通平衡树的2至3倍。
而且,splay思路与平衡树完全不同,学习splay与学习平衡树之间关联不大,没有多少互相借鉴的地方。而由于splay常数大,尽管可以完成平衡树所有操作,但是平衡树依旧有必要学习。所以,这也就加重了学习的负担。
5.treaplay思路产生与完善形成过程
首先,我们观察splay相比于普通平衡树的优秀所在:splay能够无视树的平衡性,强行改变树的结构与形态,将某一个节点提至树根而同时保证均摊复杂度是o(log2n),这样能够大大简化对区间的整体操作的难度。
反观普通平衡树,为了保证效率,而对树的结构作了强制的要求与规定,不能随意改变,导致树的结构僵化,无法自由的转动,自然也就无法将一个区间转到一颗子树中进行整体操作。比如SBT[3],将size作为平衡因子,那么,树的结构将会非常稳定,无法改变。
treap本来也是如此,在treap中强行要求heap_key满足二叉堆的性质。因此,treap的树的结构也相对稳定。但是,与SBT等其他平衡树不同的是,treap的平衡因子heap_key是由随机函数生成的,与树的结构本身没有关系。也就是说,本来我想要某一个节点作为平衡树的根节点,只要我的“运气”足够好,那个节点的heap_key正好是所有的heap_key中最小的,那么,这个treap的根节点就是我们所要的。但是,我们显然不能随机多次,直到那个节点的heap_key正好是所有的heap_key中最小的。这时,我们可以用一下小技巧:把那个我们需要的节点的heap_key值强行赋值为最小的,然后对这个treap重新维护,那个节点就成为了根节点。但是,这个技巧不能够滥用,否则对于极端情况,每一个节点的heap_key值都是人工赋值,而不是随机生成,这样,其效率也就无法保证了。
所以,对于刚刚的想法,我们还需要改进。刚刚问题在于,有些节点,之前我们想让它们作为树的根节点,但是,现在,对于这些点,我们已经没有要求了,但是,它们的heap_key值依然是之前我们人工赋值的结果。也就是说,对于没有要求的节点,我们应该保证其heap_key值的随机性,就是在对一个节点使用完成后将其heap_key值重新赋值为一个随机数。这样,我们形成了如下treaplay算法:
在普通的平衡树操作时,就与普通的treap的操作一样:
- 插入:
- 先不考虑heap_key,就像普通BST一样进行插入,然后对新节点赋予随机生成的heap_key值,然后通过单旋维护heap_key的二叉堆性质。
- 删除:
- 先将要删除的节点的heap_key值调整为正无穷,然后通过单旋维护heap_kep的二叉堆性质,此时要删除的节点将是叶子节点,直接删除即可。
- 查找:
- 和普通二叉排序树操作一样。
在涉及到区间整体操作时操作如下:
- 先将区间左端点的前驱的heap_key值赋值为负无穷,维护heap_key的二叉堆性质。
- 再将区间右端点的后继的heap_key值赋值为负无穷+1,维护heap_key的二叉堆性质。
- 此时,树根的右孩子的左孩子即为所要操作的区间,直接操作。
- 将树根的右孩子的heap_key值赋为新的随机值,维护heap_key的二叉堆性质。
- 将树根的heap_key值赋为新的随机值,维护heap_key的二叉堆性质。
6.以noi2005维护数列为例实现treaplay
维护数列[4]
【问题描述】
请写一个程序,要求维护一个数列,支持以下 6 种操作:(请注意,格式栏 中的下划线‘ _ ’表示实际输入文件中的空格)
操作名称 | 输入文件中的格式 | 说明 |
---|---|---|
插入 | INSERT_posi_tot_c1_c2_…_ctot | 在当前数列的第 posi 个数字后插入 tot个数字:c1, c2, …, ctot;若在数列首插入,则 posi 为 0 |
删除 | DELETE_posi_tot | 从当前数列的第 posi 个数字开始连续删除 tot 个数字 |
修改 | MAKE-SAME_posi_tot_c | 将当前数列的第 posi 个数字开始的连续 tot 个数字统一修改为 c |
翻转 | REVERSE_posi_tot | 取出从当前数列的第 posi 个数字开始的 tot 个数字,翻转后放入原来的位置 |
求和 | GET-SUM_posi_tot | 计算从当前数列开始的第 posi 个数字开始的 tot 个数字的和并输出 |
求和最大的子列 | MAX-SUM | 求出当前数列中和最大的一段子列,并输出最大和 |
【输入格式】
输入文件的第 1 行包含两个数 N 和 M,N 表示初始时数列中数的个数,M
表示要进行的操作数目。
第 2 行包含 N 个数字,描述初始时的数列。
以下 M 行,每行一条命令,格式参见问题描述中的表格。
【输出格式】
对于输入数据中的 GET-SUM 和 MAX-SUM 操作,向输出文件依次打印结 果,每个答案(数字)占一行。
【输入样例】
9 8
2 -6 3 5 1 -5 -3 6 3
GET-SUM 5 4
MAX-SUM INSERT 8 3 -5 7 2
DELETE 12 1MAKE-SAME 3 3 2
REVERSE 3 6
GET-SUM 5 4
MAX-SUM
【输出样例】
-1
10
1
10
【数据规模和约定】
你可以认为在任何时刻,数列中至少有 1 个数。 输入数据一定是正确的,即指定位置的数在数列中一定存在。
50%的数据中,任何时刻数列中最多含有 30 000 个数;
100%的数据中,任何时刻数列中最多含有 500 000 个数。
100%的数据中,任何时刻数列中任何一个数字均在[-1 000, 1 000]内。
100%的数据中,M ≤20 000,插入的数字总数不超过 4 000 000 个,输入文件
大小不超过 20MBytes。
treaplay代码:
#include <cstdio>
#include <iostream>
#include <stdlib.h>
#include <time.h>
using namespace std;
const int maxsize=500000;
int heap_key[maxsize+3],size[maxsize+3],parent[maxsize+3],
left_child[maxsize+3],right_child[maxsize+3];
int maxque[maxsize+3],sum[maxsize+3],number[maxsize+3],maxnum[maxsize+3],leftmax[maxsize+3],rightmax[maxsize+3];
int tagchange[maxsize+3],tagreverse[maxsize+3];
int trash[maxsize+3];
int a[maxsize+3];
const int maxn = 2147483647;
const int minn = -100;
const int null = maxsize+2;
const int bigroot = 0;
int top = null-1;
int max(int a,int b) {
if (a>b) {
return a;
} else {
return b;
}
}
void Initialize() {
parent[bigroot] = bigroot;
left_child[bigroot] = right_child[bigroot] = null;
heap_key[bigroot] =-maxn;
size[bigroot] = 1;
tagchange[bigroot] = maxn;
tagreverse[bigroot] = 0;
sum[bigroot] = number[bigroot] = maxnum[bigroot] = -maxn / 8;
maxque[bigroot] = leftmax[bigroot] = rightmax [bigroot] = 0;
parent[null] = bigroot;
left_child[null] = right_child[null] = null;
heap_key[null] = maxn;
size[null] = 0;
tagchange[null] = maxn;
tagreverse[null] = 0;
maxque[null] = leftmax[null] = rightmax[null] = sum[null] = number[null] =0;
maxnum[null] = -maxn / 8;
top=null-1;
for (int i=1;i<null;++i) trash[i]=i;
srand((int)time(0));
}
void Reverse(int t) {
if (t == null) return;
int tt = left_child[t];
left_child[t] = right_child[t];
right_child[t] = tt;
tt = leftmax[t];
leftmax[t] = rightmax[t];
rightmax[t] = tt;
tagreverse[t] = tagreverse[t] ^ 1;
}
void Change(int t,int tt) {
if (t == null) return;
sum[t] = size[t] * tt;
maxnum[t] = tagchange[t] = number[t] = tt;
if (tt > 0) {
leftmax[t] = rightmax[t] = maxque[t] = size[t] * tt;
} else {
leftmax[t] = rightmax[t] = maxque[t] = 0;
}
}
void downtag(int t) {
int lc = left_child[t];
int rc = right_child[t];
int tt = tagchange[t];
if (maxn != tt) {
Change(lc, tt);
Change(rc, tt);
tagchange[t] = maxn;
}
if (tagreverse[t]) {
Reverse(lc);
Reverse(rc);
tagreverse[t] = 0;
}
}
void update(int t) {
int lc = left_child[t];
int rc = right_child[t];
int num = number[t];
size[t] = size[lc] + size[rc] + 1;
maxque[t] = max( max(maxque[lc],maxque[rc]),rightmax[lc] + num + leftmax[rc]);
sum[t] = sum[lc] + sum[rc] + num;
maxnum[t] = max( max(maxnum[lc],maxnum[rc]),num);
leftmax[t] = max(leftmax[lc],sum[lc] + num + leftmax[rc]);
rightmax[t] = max(rightmax[rc],sum[rc] + num + rightmax[lc]);
}
void left_draft(int &t) {
downtag(t);
int tt = left_child[t];
downtag(tt);
int ttt = right_child[tt];
left_child[t] = ttt;
parent[ttt] = t;
parent[tt] = parent[t];
parent[t] = tt;
right_child[tt]=t;
update(t);
update(tt);
t=tt;
}
void right_draft(int &t) {
downtag(t);
int tt = right_child[t];
downtag(tt);
int ttt = left_child[tt];
right_child[t] = ttt;
parent[ttt] = t;
parent[tt] = parent[t];
parent[t] = tt;
left_child[tt]=t;
update(t);
update(tt);
t=tt;
}
void maintain(int t) {//维护heap_key的二叉堆性质
int tt = parent[t];
int ttt = parent[tt];
while (heap_key[t] < heap_key[tt]) {
if (right_child[ttt] == tt) {
if (right_child[tt] == t) {
right_draft(right_child[ttt]);
} else {
left_draft(right_child[ttt]);
}
} else {
if (right_child[tt] == t) {
right_draft(left_child[ttt]);
} else {
left_draft(left_child[ttt]);
}
}
tt = parent[t];
ttt = parent[tt];
}
downtag(t);
int tl = heap_key[left_child[t]],tr = heap_key[right_child[t]];
tt = parent[t];
while ((tl < heap_key[t]) || (tr < heap_key[t])) {
if (right_child[tt] == t) {
if (tl < tr) {
left_draft(right_child[tt]);
} else {
right_draft(right_child[tt]);
}
} else {
if (tl < tr) {
left_draft(left_child[tt]);
} else {
right_draft(left_child[tt]);
}
}
tl = heap_key[left_child[t]];tr = heap_key[right_child[t]];
tt = parent[t];
}
}
int Find(int s){
int t = bigroot;
while (t != null) {
if (s == size[left_child[t]] + 1) break;
downtag(t);
if (s > size[left_child[t]] + 1) {s -= size[left_child[t]] + 1; t = right_child[t];}
else t = left_child[t];
}
return t;
}
void Delete(int t) {
if (t == null) return;
trash[++top] = t;
Delete(left_child[t]);
Delete(right_child[t]);
}
int getans(int a,int b) {
if (b == 0) return a; else return b;
}
void Build(int l,int r,int &t,int deep) {
if (l == r+1) {t = null;return;}
int k = trash[top--];
t = k;
int mid = (l + r) / 2;
heap_key[k] = (rand() % (10 << deep)) + (10 << deep);
number[k] = a[mid];
tagchange[k] = maxn;
tagreverse[k] = 0;
Build(l, mid-1, left_child[k], deep+1);
Build(mid+1, r, right_child[k], deep+1);
parent[left_child[k]] = k;
parent[right_child[k]] = k;
update(k);
}
int root,b;
int main() {
freopen("sequence.in", "r" , stdin);
freopen("sequence.out", "w", stdout);
Initialize();
int n,m,pos,tot,cc;
scanf("%d %d\n",&n,&m);
for (int i=0;i<n;++i) scanf("%d",&a[i]);
Build(0, n-1, right_child[bigroot], 0);
int k = bigroot;
parent[right_child[k]] = k;
update(k);
char c[20];
while (m--) {//printall(bigroot);
scanf("%s",c);
switch (c[0]) {
case 'I':{
scanf("%d %d ",&pos,&tot);
pos++;
int t = Find(pos);
heap_key[t] = minn+1;
maintain(t);
int tt = Find(pos+1);
if (tt == null) {
for (int i=0;i<tot;++i) scanf("%d",&a[i]);
Build(0, tot-1, right_child[t], 0);
int k = t;
parent[right_child[k]] = k;
update(k);
} else {
heap_key[tt] = minn+2;
maintain(tt);
for (int i=0;i<tot;++i) scanf("%d",&a[i]);
Build(0, tot-1, left_child[tt], 0);
int k = tt;
parent[left_child[k]] = k;
update(k);
heap_key[tt] = rand() % 2000000000;
maintain(tt);
update(t);
}
heap_key[t] = rand() % 2000000000;
heap_key[bigroot] = -maxn;
maintain(t);
break;
}
case 'D':{
scanf("%d %d",&pos,&tot);
tot++;
int t = Find(pos);
heap_key[t] = minn+1;
maintain(t);
int tt = Find(pos+tot);
if (tt == null) {
Delete(right_child[t]);
right_child[t] = null;
} else {
heap_key[tt] = minn+2;
maintain(tt);
Delete(left_child[tt]);
left_child[tt] = null;
update(tt);
heap_key[tt] = rand() % 2000000000;
maintain(tt);
}
update(t);
heap_key[t] = rand() % 2000000000;
heap_key[bigroot] = -maxn;
maintain(t);
break;
}
case 'R':{
scanf("%d %d",&pos,&tot);
tot++;
int t = Find(pos);
heap_key[t] = minn+1;
maintain(t);
int tt = Find(pos+tot);
if (tt == null) {
Reverse(right_child[t]);
} else {
heap_key[tt] = minn+2;
maintain(tt);
Reverse(left_child[tt]);
update(tt);
heap_key[tt] = rand() % 2000000000;
maintain(tt);
}
update(t);
heap_key[t] = rand() % 2000000000;
heap_key[bigroot] = -maxn;
maintain(t);
break;
}
case 'G':{
scanf("%d %d",&pos,&tot);
tot++;
int t = Find(pos);
heap_key[t] = minn+1;
maintain(t);
int tt = Find(pos+tot);
if (tt == null) {
printf("%d\n",sum[right_child[t]]);
} else {
heap_key[tt] = minn+2;
maintain(tt);
printf("%d\n",sum[left_child[tt]]);
heap_key[tt] = rand() % 2000000000;
maintain(tt);
}
heap_key[t] = rand() % 2000000000;
heap_key[bigroot] = -maxn;
maintain(t);
break;
}
default:{
if (c[2] == 'X') {
printf("%d\n",getans(maxnum[right_child[bigroot]],maxque[right_child[bigroot]]));
} else {
scanf("%d %d %d",&pos,&tot,&cc);
tot++;
int t = Find(pos);
heap_key[t] = minn+1;
maintain(t);
int tt = Find(pos+tot);
if (tt == null) {
Change(right_child[t],cc);
} else {
heap_key[tt] = minn+2;
maintain(tt);
Change(left_child[tt],cc);
update(tt);
heap_key[tt] = rand() % 2000000000;
maintain(tt);
}
update(t);
heap_key[t] = rand() % 2000000000;
heap_key[bigroot] = -maxn;
maintain(t);
}
break;
}
}
}
return 0;
}
测试机器:2.7 GHz Intel Core i5
测试系统:OS X EI Capitan 10.11.1
测试结果:
treaplay | splay[5] |
---|---|
case#1 : 0.005 | case#1 : 0.005 |
case#2 : 0.007 | case#2 : 0.005 |
case#3 : 0.441 | case#3 : 0.421 |
case#4 : 0.101 | case#4 : 0.089 |
case#5 : 0.264 | case#5 : 0.218 |
case#6 : 0.460 | case#6 : 0.399 |
case#7 : 0.913 | case#7 : 0.846 |
case#8 : 0.795 | case#8 : 0.742 |
case#9 : 0.587 | case#9 : 0.605 |
case#10 : 0.640 | case#10 : 0.614 |
比较结果:treaplay略慢于splay,但是并不差太多,而且不同操作效率有差别,整体上来说效率相当。
7.treap、splay、treaplay的比较(相对于treaplay的大致比较结果)
数据结构 | 普通平衡树操作 | 区间整体操作 | 编程复杂度(关键代码部分) |
---|---|---|---|
treap | 1 | \ | 1 |
splay | 2~3 | 0.9 | 1.5 |
treaplay | 1 | 1 | 1 |
8.结论
相比而言,treaplay的效率在普通平衡树操作时与treap相当,快于splay,区间整体操作与splay相当,略慢一些,编程复杂度与treap一样,小于splay。整体上来说,treaplay不论从效率还是难度上来讲,都非常出色,可以完成到目前为止所有平衡树与splay的题目,有很强的适应性与实用性,值得推广学习。
参考资料
[1]《treap的加权形式及复杂性的严格证明》 Cecilia R.Aragon Computer Science Division University of California Berkeley Berkeley CA 94720
[2]《伸展树的原理及应用》 常州市第一中学 林厚从
[3]《SBT》陈启峰
[4]第二十二届全国信息学奥林匹克竞赛 NOI 2005 第一试 维护数列
[5]此程序引用自http://www.cnblogs.com/kuangbin/archive/2013/08/28/3287822.html