treaplay 的原理及其运用
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 行,每行一条命令,格式参见问题描述中的表格。
【输出格式】< 20000 /h4>
对于输入数据中的 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
- 计算机网络原理及其运用笔记 第六周 周阳计科1班 160809232
- KVO键值观察运用及其原理
- .NET 反射原理及其运用
- 计算机网络原理及其运用 第七周 周阳 160809232
- ZooKeeper 原理及其在 Hadoop 和 HBase 中的应用
- ASP.NT运行原理和页面生命周期详解及其应用
- MFC中CArray类原理及其应用
- MySql查询优化及其原理
- js事件委托及其原理
- Linux Bond的原理及其不足
- eclipse反射原理及运用
- 网络地址转换NAT原理及其作用
- 线程特定数据TSD及其实现原理
- SSH原理与运用(一):远程登录
- 基于CNN网络的汉字图像字体识别及其原理
- CSS-三角形及其原理
- Linux安全管理-Iptables原理及其应用
- 九宫格的认识以及如何运用九宫格原理
- 有效降低内存峰值防止过高及其原理分析
- mmap原理及其在ART中的应用(1)