FHQ Treap:不用旋转的treap,还能维护区间!

FHQ(范浩强) Treap:利用了treap的结构(每个节点上的一个新值域整体上满足堆性质),却简化了很多操作(不用旋转),核心操作2个函数,助您深刻理解“函数式编程”的意义!

提前说明一下,为了方便,FHQtreap中每个节点只存一个值,即就算有若干相同的值,他们也要建若干节点存起来而不是只建一个节点存。

核心操作:

1、分裂(split):

分裂分两种:按值和按排名。

按值分裂:按值x将1个treap分成2个,其中一个所有节点的键值小于等于x,另一个则大于x的treap。这里分裂过程是在原treap上从上到下一部分一部分拆开合成两个treap的。

  从原树根节点开始看当前节点,若当前节点的键值小于等于x,则可以把当前节点及其左子树归到“小树”里,再看它的右子树;若当前节点的键值大于x,则可以把当前节点及其右子树归到“大树”里,再看它的左子树。

void split(int now,int x,int &u,int &v)
{
    if(!now)
    {
        u=v=0;
        return;
    }
    if(num[now]<=x)
    {
        u=now;
        split(tre[now][1],x,tre[now][1],v);
    }
    else
    {
        v=now;
        split(tre[now][0],x,u,tre[now][0]);
    }
    updata(now);
}

按排名(个数)分裂:思路与上类似,注意看右子树是排名要减(左子树的大小+1)

void splith(int now,int k,int &u,int &v)
{
    if(!now)
    {
        u=v=0;
        return;
    }
    if(siz[tre[now][0]]>=k)
    {
        v=now;
        splith(tre[now][0],k,u,tre[now][0]);
    }
    else
    {
        u=now;
        splith(tre[now][1],k-siz[tre[now][0]]-1,tre[now][1],v);
    }
    updata(now);
}

2、合并(merge):

将两棵树合并,其中小树所有节点键值都小于大树(这是重点),从两个树的根节点看起当前两个节点,若小根的强化值小于等于大根的强化值,则小根及其左子树成为新树的一部分,新右子树为小根的右子树与剩下的大树merge后的结果;若小根的强化值大于大根的强化值,则大根及其右子树成为新树的一部分,新左子树为大根的左子树与剩下的小树merge后的结果。这样满足treap的二叉查找树和堆的性质。

int merge(int u,int v)
{
    if(!u||!v)
        return u+v;
    if(dev[u]<=dev[v])
    {
        tre[u][1]=merge(tre[u][1],v);
        updata(u);
        return u;
    }
    else
    {
        tre[v][0]=merge(u,tre[v][0]);
        updata(v);
        return v;
    }    
}

一定要注意merge()的第一个参数传的是当前小树的根,第二个参数传的是当前大树的根。

(3) 查找排名第k大的数(辅助函数)

与二叉搜索树无异。

int fin(int u,int k)
{
    if(siz[tre[u][0]]>=k)
        return fin(tre[u][0],k);
    if(siz[tre[u][0]]+1==k)
        return num[u];
    return fin(tre[u][1],k-siz[tre[u][0]]-1);
}

实现操作:

几乎所有操作都能由上面三个函数配合完成,代码很直白

1、插入数x;2、删除数x;3、查询数x的排名;4、查找排名为x的数;5、求数x前驱;6、求数x后继。

                opt=read(),x=read();
        switch(opt)
        {
            case 1:
                split(root,x,u,v);
                v=merge(newnode(x),v);
                root=merge(u,v);
                break;
            case 2:
                split(root,x,u,v);
                split(u,x-1,u,w);
                w=merge(tre[w][0],tre[w][1]);
                u=merge(u,w);
                root=merge(u,v);
                break;
            case 3:
                split(root,x-1,u,v);
                printf("%d\n",siz[u]+1);
                root=merge(u,v);
                break;
            case 4:
                printf("%d\n",fin(root,x));
                break;
            case 5:
                split(root,x-1,u,v);
                printf("%d\n",fin(u,siz[u]));
                root=merge(u,v);
                break;
            case 6:
                split(root,x,u,v);
                printf("%d\n",fin(v,1));
                root=merge(u,v);
                break;
        }            

注意一般情况下,修改/查询操作的最后要再将所有分裂的树合并起来。

7、区间操作:

此时节点的键值存的是区间下标,若要修改/查询区间[l,r]的元素,只需将原树按r分裂,再对小树按l-1分裂,这是分裂出的大树即为[l,r],此时再输出查询结果/打标记就行了。若一个点有标记,则在其子树增或减点时都要下传标记。

 说一下用平衡树维护区间的注意点:

  若用平衡树维护区间,分裂多是按排名(个数)分裂。因为所有操作都不会改变它二叉搜索树的性质,所以用排名代表下标就好。

  插入:若要对一个完整序列建立平衡树,可顺序merge到树里。若要再第i个数后插入一个数x,可先按排名i分裂成两树,加上新节点,逐一merge起来就好。

  其他操作类似。其实用平衡树维护区间的话,它用于满足二叉搜索树的性质的键值就是元素在序列中的下标。不过,因为所有操作都不会改变它二叉搜索树的性质,所以甚至连这个键值都不用花内存存,直接存要维护的元素的数据就好。

   可联系一下splay的相关部分:Splay详解

遇到的问题(坑)

  rand()范围: 0~32767(RAND_MAX)

  const int &x作为函数形参,函数中不能对x直接赋值,但可以利用地址修改x的值。不要认为有了const int &x后x的值就一定不会被手残改掉。

posted @ 2020-06-09 22:04  千叶繁华  阅读(253)  评论(0编辑  收藏  举报