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)

二叉排序树是一种有序二叉树,定义如下:
  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 行,每行一条命令,格式参见问题描述中的表格。

【输出格式】

对于输入数据中的 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
测试结果:

treaplaysplay[5]
case#1 : 0.005case#1 : 0.005
case#2 : 0.007case#2 : 0.005
case#3 : 0.441case#3 : 0.421
case#4 : 0.101case#4 : 0.089
case#5 : 0.264case#5 : 0.218
case#6 : 0.460case#6 : 0.399
case#7 : 0.913case#7 : 0.846
case#8 : 0.795case#8 : 0.742
case#9 : 0.587case#9 : 0.605
case#10 : 0.640case#10 : 0.614

比较结果:treaplay略慢于splay,但是并不差太多,而且不同操作效率有差别,整体上来说效率相当。

7.treap、splay、treaplay的比较(相对于treaplay的大致比较结果)

数据结构普通平衡树操作区间整体操作编程复杂度(关键代码部分)
treap1\1
splay2~30.91.5
treaplay111

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值