您的位置:首页 > 其它

treaplay 的原理及其运用

2016-04-10 21:43 302 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/baidu_34122416/article/details/51115454

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)

二叉排序树是一种有序二叉树,定义如下:
  1. 空树是一颗二叉排序树。
  2. 如果一颗二叉树根节点的左子树、右子树都是二叉排序树,并且左子树所有节点的排序关键字的值 小于 根节点的排序关键字的值 小于 右子树所有节点的排序关键字的值,那么,这颗二叉树是一颗二叉排序树。

不难发现,二叉排序树有一个非常好的性质:二叉排序树的中序遍历得到的排序关键字的顺序是升序的。这个性质非常实用,是我们使用并且不断改良二叉排序树的原因。

至于二叉排序树的各种操作,不是本文重点,不再赘述。

我们发现,在普通情况下,二叉排序树的深度是在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的二叉堆性质,此时要删除的节点将是叶子节点,直接删除即可。
  • 查找:
      和普通二叉排序树操作一样。

    在涉及到区间整体操作时操作如下:

    1. 先将区间左端点的前驱的heap_key值赋值为负无穷,维护heap_key的二叉堆性质。
    2. 再将区间右端点的后继的heap_key值赋值为负无穷+1,维护heap_key的二叉堆性质。
    3. 此时,树根的右孩子的左孩子即为所要操作的区间,直接操作。
    4. 将树根的右孩子的heap_key值赋为新的随机值,维护heap_key的二叉堆性质。
    5. 将树根的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

  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: