可并堆——左偏树斜堆

Posted Halifuda

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了可并堆——左偏树斜堆相关的知识,希望对你有一定的参考价值。

经典的二叉堆已经可以在O(logn)的复杂度的情况下维护堆这样的数据结构,也有d-堆可以维护成O(logdn)(虽然pop操作的复杂度是dlogdn),然而这两种堆不能满足logn的合并操作,它们的经常是O(nlogn),即每次将一个堆中的堆顶拿出来放到另一个堆里。虽然有很多情况不经常合并,但有时候我们就是想要合并堆,还想频繁地合并,这时候二叉堆的性能就显得不是很好了。左偏树首先解决了合并问题。

左偏树像二叉堆一样是个二叉树,但是并不是完全二叉树。左偏树定义时用到了一个附加值:nplnpl[x]指节点x通向NULL的最短路径。一般NULLnpl-1;若A只有一个子节点或者没有,那么npl[A]=0;否则,npl[A]=min{npl[left[A]],npl[right[A]]}+1。乍一看像是高度的定义。

左偏树的定义如下:对于左偏树T,要么只有一个节点,要么左右子树LR满足:npl[L]≥npl[R]

满足条件的左偏树有性质:对于树根T,总有:npl[T]=npl[right[T]]+1。对于T,若npl[T]=l,则整棵树的节点数不少于2l-1

性质1很好得出;性质二书上利用数学归纳法。不过简单讲就是左子树的npl大于等于l-1,一直取左子树npl=l-1,由性质1得右子树npl=l-1,可得左右子树全等,那么这就是一棵满二叉树,n=2l-1,由于左子树总是可以多任意多节点,所以这样就可以得到性质二。性质二反过来讲相当于若Tn个节点,那么右路径的长度不超过logn(右路径为从根一直通向右子树直到NULL的路径)。左偏树满足这个性质后,总是把合并放到右路径上解决,这样保证了合并也是O(logn)的时间复杂度。

合并是这样的:合并两棵树时,令根键值小树的根为新根,再递归合并新根的右子树和另一棵树。回溯时若发现某一结点不满足左偏树的定义,即左子树的npl小于右子树的npl,那么交换左右子树,这样合并操作总能保持O(logn)。这样做之后,插入即合并一个节点和原树,删除堆顶即合并树根的两个子树作为新根。

代码:

技术分享图片
#include<cstdio>
#define min(a,b) (a<b?a:b)
#define nil 0
#define MXN 100000+1
int val[MXN],left[MXN],right[MXN],npl[MXN],recycle[MXN];
int root,ntop,rtop=-1;
int newnode(int k){
    int nw;
    if(rtop==-1) nw=++ntop;
    else nw=recycle[rtop--];
    val[nw]=k;
    left[nw]=right[nw]=nil;
    npl[nw]=1;
    return nw;
}
void update(int now){
    npl[now]=min(npl[left[now]],npl[right[now]])+1;
    return;
}
void swap(int &a,int &b){
    int t=a;
    a=b,b=t;
    return;
}
int merge(int A,int B);
int merge1(int A,int B);
int merge(int A,int B){
    if(A==nil) return B;
    if(B==nil) return A;
    if(val[A]<val[B]) return merge1(A,B);
    else return merge1(B,A);
}
int merge1(int A,int B){
    if(left[A]==nil) left[A]=B;
    else{
        right[A]=merge(right[A],B);
        if(npl[left[A]]<npl[right[A]]) swap(left[A],right[A]);
        npl[A]=npl[right[A]]+1;
    }
    return A;
}
void insert(int k){
    int ins=newnode(k);
    if(root==nil) root=ins;
    else root=merge(root,ins);
    return;
}
int top(){
    return val[root];
}
void pop(){
    recycle[++rtop]=root;
    int y=merge(left[root],right[root]);
    root=y;
    return;
}
int main(){
    int p,x;
    while(1){
        scanf("%d",&p);
        if(p==0) break;
        if(p==1) printf("%d\n",top());
        if(p==2){
            scanf("%d",&x);
            insert(x);
        }
        if(p==3) pop();
    }
    return 0;
}
Leftist Heap

另外讲一下左偏树的变种:斜堆。斜堆只是把npl这个限制条件拿掉,每次合并不论情况总是交换左右子树,这样使得期望的复杂度是O(logn)。像Splay一样,斜堆是自调整式数据结构,不需要记录任何额外变量。但是斜堆存在的劣势是右路径有时可能变得较长,面对较大数据递归时可能超过深度。不过左偏树和斜堆都可以写出非递归的程序,所以这不是太大的问题。

注:虽然在堆里实现查找多数情况下不现实,但是有时候堆还是被要求具有更改某些节点键值的不合理操作。如果想要具有减小键值这一操作(暂时不管如何找到应该减小键值的节点),那么左偏树或者斜堆都要记录父亲指针,进行向上调整。不过这无法保证对数时间复杂度。这是更强大的可并堆出现的原因之一。


以上是关于可并堆——左偏树斜堆的主要内容,如果未能解决你的问题,请参考以下文章

并不对劲的左偏树

左偏树

P3377 模板左偏树(可并堆) 左偏树浅谈

左偏树(可并堆)

luogu_P3377 左偏树(可并堆)

模板左偏树(可并堆) 可并堆_并查集