TREAP

TREAP

Treap = Tree + Heap.
树堆,在数据结构中也称Treap,是指有一个随机附加域知足堆的性质的二叉搜索树,其结构至关于以随机数据插入的二叉搜索树。其基本操做的指望时间复杂度为O(logn)。相对于其余的平衡二叉搜索树,Treap的特色是实现简单,且能基本实现随机平衡的结构。html

Treap 维护堆的性质的方法只用到了左旋和右旋, 编程复杂度比Splay小一点(??), 而且在二者可完成的操做速度有明显优点node

然而一个裸的treap是不能支持区间操做的,因此可能功能没有splay那么强大.编程

treap的操做方式和splay差很少,但由于treap的结构是不能改变的,而splay的形态能够随意改变,因此在实现上会有一点小区别.数据结构

treap具备如下性质:

  1. Treap是关于key的二叉排序树(也就是规定的排序方式)。
  2. Treap是关于priority的堆(按照随机出的优先级做为小根/大根堆)。(非二叉堆,由于不是彻底二叉树)
  3. key和priority肯定时,treap惟一。

为何除了权值还要在用一个随机值做为排序方法呢?随机分配的优先级,使数据插入后更不容易退化为链。就像是将其打乱再插入。因此用于平衡二叉树。函数

首先是treap的定义:ui

struct treap{
	int ch[2], cnt, size, val, rd;
	//treap不须要记录父指针,rd表示节点的随机值
}t[N];

更新

直接统计子树大小.url

void up(int x){
	t[x].size = t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
}

旋转

treap须要支持平衡树的性质,因此是须要用到旋转的.这里旋转的方法是和splay的旋转方法是同样的,就不贴图了.由于treap中并无记录父节点,因此须要传一个参数表示旋转方向.spa

void rotate(int &x, int d){//x表明的是旋转时做为父节点的节点,d表明的是旋转的方向
//d==0时是左儿子旋上来, d==1是右儿子旋上来.
    int son = t[x].ch[d];
    t[x].ch[d] = t[son].ch[d^1];
    t[son].ch[d^1] = x; up(x), up(x=son);//至关于up(son)
}

插入/删除

由于treap在其余操做过程当中是并不改变树的形态的,因此在插入或是删除时要先找到要插入/删除的节点的位置,而后再建立一个新的节点/删除这个节点..net

而后考虑到插入/删除后树的形态有可能会改变,因此要考虑要经过旋转维护treap的形态.咱们这里分类讨论一下: 插入指针

  • 能够直接按照中序遍历结果找到最终的对应位置,而后再经过随机值维护它堆的性质.
void insert(int &x, int val){
    if(!x){//找到对应位置就新建节点
        x = ++cnt;
        t[x].cnt = t[x].size = 1;
        t[x].val = val, t[x].rd = rand();
        return;
    }
    t[x].size++;//由于插入了数,因此在路径上每一个节点的size都会加1
    if(t[x].val == val){t[x].cnt++; return;}//找到了直接返回
    int d = t[x].val < val; insert(t[x].ch[d], val);//不然递归查找插入位置
    if(t[x].rd > t[t[x].ch[d]].rd) rotate(x, d);
}

删除

  • 先找到要删除的节点的位置.
  • 若是这个节点位置上有多个相同的数,则直接cnt--.
  • 若是只有一个儿子或者没有儿子,直接将那个儿子接到这个节点下面(或将儿子赋值为0).
  • 若是有两个儿子,现将随机值小的那个旋到这个位置,将根旋下去,而后将旋以后的状况转化为前几种状况递归判断.
void delet(int &x, int val){
    if(!x) return;//防止越界
    if(t[x].val == val){
        if(t[x].cnt > 1){t[x].cnt--, t[x].size--;return;}//有相同的就直接cnt--
        bool d = t[ls].rd > t[rs].rd;
        if(ls == 0 || rs == 0) x = ls+rs;//只有一个儿子就直接把那个儿子放到这个位置
        else rotate(x, d), delet(x, val);//不然将x旋下去,找一个随机值小的替代,直到回到1,2种状况
    }
    else t[x].size--, delet(t[x].ch[t[x].val<val], val);//递归找到要删除的节点.
}

查排名

仍是由于treap不能改变形态,因此不能像splay同样直接找到这个点旋转到根,因此咱们用递归的方式求解,咱们用到的目前这个点的权值做为判断的依据,并在找到节点的路上不断累加小于该权值的个数.

看代码理解一下吧.

int rank(int x, int val){
    if(!x) return 0;
    if(t[x].val == val) return t[ls].size+1;//找到了就返回最小的那个
    if(t[x].val > val) return rank(ls, val);//若是查找的数在x的左边,则直接往左边查
    return rank(rs, val)+t[ls].size+t[x].cnt;//不然往右边查,左边的全部数累加进答案
}

查找第k小的数

由于只须要找到中序遍历中的第k个,因此在找第k小的时候能够直接用splay同样的方法,也是递归求解.

int kth(int root, int k){
    int x = root;
    while(1){
        if(k <= t[ls].size) x = ls;
        else if(k > t[ls].size+t[x].cnt)
            k -= t[ls].size+t[x].cnt, x = rs;
		else return t[x].val;
    }
}

查找前驱/后继

仍然是由于不能改变树的形态,须要递归求解.一样的找一个节点的前驱就直接在它左半边中找一个最大值就能够了.若是是在这个节点的右边的话就一直向下递归,若是递归有一个分支直到叶子节点如下都一直没找到一个比该权值要小的值,那么最后要返回一个-inf/inf来防止答案错误(同时找到叶子节点下面也是要及时return防止越界).

int pre(int x, int val){
    if(!x) return -inf;//防止越界,同时-inf没法更新答案,
    if(t[x].val >= val) return pre(ls, val);//若是该节点的权值大于等于要找的权值
    //则不能成为前驱,递归查找左子树(有可能找到前驱)
    return max(pre(rs, val), t[x].val);//找右子树中是否存在前驱
}

int nex(int x, int val){//同上
    if(!x) return inf;
    if(t[x].val <= val) return nex(rs, val);
    return min(nex(ls, val), t[x].val);
}

既然treap有这么多不能实现的操做,那这个treap有什么用呢?

显然是有的,咱们由于支持旋转的treap不能改变树的形态来完成操做,因此这里介绍一中更增强大的数据结构:

无旋treap

简介

无旋treap具备treap的一些性质,好比二叉树和堆的性质,同时也是一颗平衡树.

无旋treap是怎么个无旋的方法的呢?其实相比于带旋转的treap,无旋treap只是多了两个特殊的操做:splitmerge .

那么这两个操做究竟是什么这么厉害呢?说简单点,就是一个分离子树和一个合并子树的过程.

咱们能够用split操做分离出1~前k个节点,这样就能够经过两次split操做就能够提取出任意一段区间了.

而merge操做能够将两个子树合并,并同时维护好新合并的树的treap全部的性质.

下面终点讲一下这两个操做:

split

首先看一张split的动图: 操做过程如上,节点中间的值表明权值,右边的数字表明随机值. 咱们在操做的过程当中须要将一颗树剖成两颗,而后为了还能进行以后的操做,分离出的两颗字数必须也是知足性质的,为了找到这两颗子树,咱们在分离的过程当中须要记录下这两颗子树的根.

从图中能够看出,其实这个分离的操做也能够理解为将一棵树先剖开,而后再按照必定的顺序链接起来,也就是将从x节点一直到最坐下或是最右下剖出来,而后再继续处理剖出来链的剩余部分.

看代码理解一下吧.

void split(int x, int k, int &a, int &b){//开始时a,b传的是用来记录两颗子树根的变量
//x表明目前操做到了哪个节点,k是要分离出前k个节点
	if(!x){a = b = 0; return;}//若是没有节点则须要返回
	if(k <= t[ls].size)	b = x, split(ls, k, a, ls);//若是第k个在左子树中
	//则往左走,同时左子树的根就能够肯定了,那么就把ls赋值为根.
	//同时为了以后要把接下来处理出的节点再连上去,要再传ls做为参数,将以后改变为根的接到如今的x的左儿子
	else a = x, split(rs, k-t[ls].size-1, rs, b);//同理
}

固然,要实现查找前驱后继的话能够不用分离前k个节点的方法来分离,能够直接按照权值来分离节点,方法相似.

void split(int x, int val, int &a, int &b){
    if(!x){a = b = 0; return;}
    if(t[x].val <= val) a = x, split(rs, val, rs, b);
    //若是带等于就是把>val的节点分到第二颗子树中.
    //不然就是将<=val的节点分到第一颗子树中.
    else b = x, split(ls, val, a, ls); up(x);
}

merge

注:图中最后插入的9位置错了,应该是在8的右下. 首先merge操做是有前提条件的,要求是必须第一颗树权值最大的节点要大于第二棵树权值最小的节点.

由于有了上面那个限制条件,因此右边的子树只要是插在左边的这颗树的右儿子上就能够维护它的中序遍历,那么咱们就只须要考虑如何维护它平衡树的性质.

这里咱们就须要经过玄学的随机值来维护这个树的性质了.咱们在合并两颗树时,由于左边的权值是必定小于右边的,因此左边的那棵树必定是以一个根和它的左子树的形式合并的,而右边的那棵树就是以根和右子树的形式合并的,那么若是此次选择的是将左边的树合并上来的话,那么下一次合并过来的位置必定是在这个节点位置的右儿子位置(能够看图理解一下).

你能够把这个过程理解为在第一个Treap的左子树上插入第二个树,也能够理解为在第二个树的左子树上插入第一棵树。由于第一棵树都知足小于第二个树,因此就变成了比较随机值来肯定树的形态。

int merge(int x, int y){
    if(x == 0 || y == 0) return x+y;
    if(t[x].rd < t[y].rd){//最好merge函数不要用宏定义的变量
//由于这个比较的两颗树的根的随机值,用宏定义容易写错
        t[x].ch[1] = merge(t[x].ch[1], y);
        up(x); return x;
    }
    else {
        t[y].ch[0] = merge(x, t[y].ch[0]);
        up(y); return y;
    }
}

到这里两个核心操做就完成了,那么那些insert,delete,get_rank,get_pre,get_nex的操做该怎么作呢?其实很简单,就考虑一下将这颗树如何分离,而后从新合并好就能够了.

插入

插入仍是老套路,先找到位置而后插入.这里咱们能够先分离出val和它以前的节点,而后把val权值的节点加到第一颗树的后面,而后合并.

void insert(int val){
    split(root, val, r1, r2);
    root = merge(r1, merge(newnode(val), r2));
}

至于newnode的话就直接新建一个节点就能够了.

int newnode(int val){
    t[++cnt].val = val; t[cnt].rd = rand(), t[cnt].size = 1;
    return cnt;
}

删除

先将val和它前面的权值分离出来,用r1记录这个根,再在r1树中分离出val-1的权值的树,用r2记录这颗树,那么val这个权值必定是已经被分离到以r2为根的树中,删掉这个数(能够直接把这个位置的节点用它左右儿子合并后的根代替),最后将分离的这几颗树按顺序合并回去就能够了.

void delet(int val){
    r1 = r2 = r3 = 0; split(root, val, r1, r3);
    split(r1, val-1, r1, r2);
    r2 = merge(t[r2].ch[0], t[r2].ch[1]);
    root = merge(r1, merge(r2, r3));
}

查找某数的排名

能够直接将全部比它小的权值分离到一颗树中,那么此时排名就是这颗树的大小+1了.

int rank(int val){
    r1 = r2 = 0; split(root, val-1, r1, r2);
    ans = t[r1].size+1;
    root = merge(r1, r2);
    return ans;
}

查找第k小的数

能够直接在整颗树中直接找,操做方法相似splay.

int kth(int rt, int k){
    int x = rt;
    while(1){
        if(k <= t[ls].size) x = ls;
        else if(k > t[ls].size+t[x].cnt)
            k -= t[ls].size+t[x].cnt, x = rs;
        else return x;
    }
}

查找前驱/后继

以val-1为分界线将整棵树分离开,那么前驱就是第一颗树中的最大的数字(严格比val小). 以val为分界线将整颗树分离开,后继就是第二颗树中的最小的数字(严格比val大).

int pre(int val){
    r1 = r2 = 0; split(root, val-1, r1, r2);
    ans = t[kth(r1, t[r1].size)].val;
    root = merge(r1, r2);
    return ans;
}

int nex(int val){
    r1 = r2 = 0; split(root, val, r1, r2);
    ans = t[kth(r2, 1)].val;
    root = merge(r1, r2);
    return ans;
}

经过节点编号找节点在中序遍历中的位置

由于treap自己是不能经过编号来进行操做的,它只能经过$split$来分离子树,因此在只知道节点编号的时候不能很快找到节点的位置.

因此为了方便寻找节点.咱们在treap中多记录一个$father$,而后就能够经过不断的向上跳来记录中序遍历比它小的节点的个数.

而中序遍历比它小的节点也就是在它左子树中的节点或是在它祖先的左子树中的节点.咱们只须要在向根跳的时候将全部左子树记录下来就能够了.可是由于有可能某个节点是根的左儿子,这样在向上跳的时候就不用重复统计答案了.

int find(int cnt){
    int node = cnt, res = t[t[cnt].ch[0]].size+1;
    while(node != root && cnt){
		if(get(cnt)) res += t[t[t[cnt].fa].ch[0]].size+1;
		cnt = t[cnt].fa;
    }
    return res;
}

而后咱们须要考虑$father$的修改.由于会改变树的形态的只有$split$和$merge$操做,因此只须要在这两个函数进行修改就能够了.其实很简单,只要在修改儿子的同时修改父亲就能够了.

$split$

void split(int x, int k, int &a, int &b, int faa = 0, int fab = 0){
    if(x == 0){ a = b = 0; return; }
    if(k <= t[t[x].ch[0]].size) t[x].fa = fab, b = x, split(t[x].ch[0], k, a, t[x].ch[0], faa, x);
    else t[x].fa = faa, a = x, split(t[x].ch[1], k-t[t[x].ch[0]].size-1, t[x].ch[1], b, x, fab); up(x);
}

$merge$

int merge(int x, int y){
    if(x == 0 || y == 0) return x+y;
	    if(t[x].rd < t[y].rd){
	    t[x].ch[1] = merge(t[x].ch[1], y);
	    t[t[x].ch[1]].fa = x; up(x); return x;
    }
    else {
	    t[y].ch[0] = merge(x, t[y].ch[0]);
	    t[t[y].ch[0]].fa = y; up(y); return y;
    }
}

如何在$O(n)$时间内构造一颗平衡treap

能够考虑用栈实现. 每次将一个节点压入栈中,为了知足构造的这颗平衡树的中序遍历是按照咱们的输入顺序的,因此每次插入的节点必定是在以前构造的部分的右边的.

因此这里用了一个栈,每次加入一个节点就把新节点编号加入栈.而且栈中的元素都在栈的右边.

简单点说,也就是若是不考虑这个随机值的话,构造的平衡树就是一条链.从栈顶到栈底是一条从下到上的链,像这样:

可是咱们显然是不能让它构造一条链的,因此咱们经过随机值来判断何时要将这条链缩短,并把这条连接到新的根的右儿子上.咱们再假设如今已经插入了4个数,而且随机值正好是知足它成为一条链的状况的,那么该如何插入5呢?(节点边上的括号内的值是随机值).

显然按照随机值维护堆的性质的原理,5节点应该是要到1节点的下面的,那么那一重$while$循环就会进行判断,并保存$last$最后到哪一个位置.(以下图)

显然咱们这样能够将一条链分开来,而且不会改变它原有的链.最后将节点按顺序接上,这样新插入一个节点就完成了.

以后每次插入节点都是这个方法,这样造出的平衡树也是相对平衡的(随机值保证了树平衡的性质).

复杂度的话虽然里面套了个$while$,可是每一个点都是只会入栈出栈一次的,因此均摊下来是$O(n)$的.

int build(int len){
    for(int i=1;i<=len;i++){
        int temp = newnode(s[i]), last = 0;
        while(top && t[stk[top]].rd > t[temp].rd)
            up(stk[top]), last = stk[top], stk[top--] = 0;
        if(top) t[stk[top]].ch[1] = temp;
        t[temp].ch[0] = last, stk[++top] = temp;
    }
    while(top) up(stk[top--]); return stk[1];
}

总结

无旋treap到这里就差很少讲完了,感受这个确实是很好用的,起码这个调试难度比其余的平衡树要简单一些.

分离区间什么的操做也是这个道理,能够本身想一下.

例题固然仍是跳到这里看呀.

相关文章
相关标签/搜索