堆/左偏树

堆(二叉堆)

定义

是完全二叉树,每个点有一 \(val\) ,且满足,对于给定的比较函数我们有,对于任意一个点,其与其任意儿子间的关系都满足该关系。

例:大根堆的每个点都大于它的任意儿子,小根堆则每个点都小于它的任意儿子。

STL 中有 priority_queue<> 实现了该功能。

下面以小根堆为例。别的堆可以自定义比较函数得到。

功能

一个堆支持如下操作:

  1. 查询堆顶元素
  2. 删除堆顶元素
  3. 插入新的元素

实现

我们需要动态维护两个性质:完全二叉树和每个儿子大于其父亲。

1.查询堆顶元素

由于我们始终保持其堆的性质,所以根节点即为堆顶元素,返回即可。

2.删除堆顶元素

我们先 swap(root,lastnode) ,并将 lastnode 位置删除,这样我们就删除了指定元素,并保持了完全二叉树的性质。但是我们遇到另外一个问题,我们在这一操作中破坏了其父子节点大小关系,这就需要我们进行调整。注意到不合法的位置有且仅有我们刚刚换上去的根,于是我们可以在它的两个儿子中选一个较小的,并将该点与其交换。但是这样堆可能仍不合法,我们只需对该子树重复该操作即可。(当然,如果堆已合法,我们不需再调整)由于其为完全二叉树,我们最多调整 \(\log n\) 次。

3.插入新的元素

我们将新加的元素直接放在完全二叉树上的第一个合法位置。然后我们又遇到了不合法的问题,我们可以参考操作二中的调整,只不过这次是自底向上调整,复杂度相同,也为 \(O(\log n)\)

一个支持上述三种操作的示例
struct heap
{
    int a[mn],tp;
    heap()
    {
        tp=0;
    }
    void mvup(int pos)
    {
        while(pos!=1 && a[pos>>1]>a[pos])
        {
            swap(a[pos>>1],a[pos]);
            pos=pos>>1;
        }
    }
    void mvdn(int pos)
    {
        while((pos<<1)<=tp)
        {
            if((pos<<1)==tp)
            {
                if(a[pos<<1]<a[pos])swap(a[pos],a[pos<<1]);
                break;
            }
            if(a[pos<<1]<a[pos<<1|1])
            {
                if(a[pos<<1]<a[pos])swap(a[pos],a[pos<<1]);
                else break;
                pos<<=1;
            }
            else
            {
                if(a[pos<<1|1]<a[pos])swap(a[pos],a[pos<<1|1]);
                else break;
                pos=pos<<1|1;
            }
        }
    }
    void push(int x)
    {
        a[++tp]=x;
        mvup(tp);
    }
    void pop()
    {
        swap(a[1],a[tp]);
        tp--;
        mvdn(1);
    }
    int top()
    {
        return a[1];
    }
}q;

左偏树

这是一种可并堆。下面给出 OI-wiki 上的定义。

对于一棵二叉树,我们定义 外节点 为子节点数小于两个的节点,定义一个节点的 \(dist\) 为其到子树中最近的外节点所经过的边的数量。空节点的 \(dist\)\(0\)
左偏树是一棵二叉树,它不仅具有堆的性质,并且是「左偏」的:每个节点左儿子的 \(dist\) 都大于等于右儿子的 \(dist\)

其核心操作为 join ,即将两个点及其子树合并。下面给出以小根堆为实例的代码及注释。

node* join(node* x,node *y)//返回指针方便父亲找儿子
{
    if(!x || !y)return x?x:y;//如果有至少一个是空的,那就不用合并了,将非空的作为合并好的即可
    node *res=NULL;
    if(x->val>y->val || (x->val==y->val && x->id>y->id))swap(x,y);//优先选小的点作为根,这里还要比id是因为洛谷板子要求的
    res=join(x->ch[1],y);//合并右子和非根的那一部分
    x->ch[1]=res;//将合并的子树接在x的右儿子上
    if(res)res->fa=x;
    if(x->ch[1] && (!x->ch[0] || x->ch[1]->dist>x->ch[0]->dist))swap(x->ch[0],x->ch[1]);//判断dist是否满足要求,不满足则需要交换左右儿子
    x->dist=(x->ch[1]?x->ch[1]->dist+1:0);//更新x的dist
    x->fa=NULL;
    return x;
}

然后别的操作我们可以以 join 为基础实现:

  1. 加入点:将新点看作一个堆,与原堆合并
  2. 删除点:合并该点的左右儿子
支持合并,查堆顶,删堆顶,加点的堆的左偏树实现
struct letree
{
    struct node
    {
        node *fa,*ch[2];
        int dist,val,id;
        // int sz;
        node(int x,int nid)
        {
            dist=0,val=x;
            id=nid;
            // sz=1;
            fa=ch[0]=ch[1]=NULL;
        }
    }*root;
    // int sz;
    letree()
    {
        // sz=0;
        root=NULL;
    }
    node* join(node* x,node *y)
    {
        if(!x || !y)return x?x:y;
        node *res=NULL;
        if(x->val>y->val || (x->val==y->val && x->id>y->id))swap(x,y);
        res=join(x->ch[1],y);
        x->ch[1]=res;
        if(res)res->fa=x;
        if(x->ch[1] && (!x->ch[0] || x->ch[1]->dist>x->ch[0]->dist))swap(x->ch[0],x->ch[1]);
        x->dist=(x->ch[1]?x->ch[1]->dist+1:0);
        x->fa=NULL;
        return x;
    }
    void join(letree &x)
    {
        root=join(root,x.root);
        x.root=NULL;
    }
    bool empty()
    {
        return root==NULL;
    }
    void push(int x,int id)
    {
        node *o=new node(x,id);
        root=join(root,o);
    }
    int top()
    {
        return empty()?-1:root->val;
    }
    void pop()
    {
        deld[root->id]=1;
        root=join(root->ch[0],root->ch[1]);
    }
}q[mn];
支持合并,查堆顶,删堆顶,加点,删除指定元素,调整某一元素大小的更为强大的左偏树实现
struct node
{
    node *fa,*ch[2];
    int dist,val;
    // int sz;
    node(int x)
    {
        dist=0,val=x;
        // sz=1;
        fa=ch[0]=ch[1]=NULL;
    }
    bool isr()
    {
        return fa->ch[1]==this;
    }
}*now[mn];//用于存储各元素位置,方便erase
int nodecnt=0;
struct letree
{
    node *root;
    letree()
    {
        root=NULL;
    }
    node* join(node* x,node *y)// 核心
    {
        if(!x || !y)return x?x:y;
        if(x->val>y->val)swap(x,y);
        x->ch[1]=join(x->ch[1],y);
        if(x->ch[1])x->ch[1]->fa=x;
        if(x->ch[1] && (!x->ch[0] || x->ch[1]->dist>x->ch[0]->dist))swap(x->ch[0],x->ch[1]);
        x->dist=(x->ch[1]?x->ch[1]->dist+1:0);
        x->fa=NULL;
        return x;
    }
    void modup(node *x)//用于在erase后向上调整以保持左偏性质
    {
        while(x)
        {
            if(x->ch[1] && (!x->ch[0] || x->ch[1]->dist>x->ch[0]->dist))
            {
                swap(x->ch[0],x->ch[1]);
                x->dist=(x->ch[1]?x->ch[1]->dist+1:0);
                x=x->fa;
            }
            else break;
        }
    }
    void join(letree &x)//合并
    {
        root=join(root,x.root);
        x.root=NULL;
    }
    bool empty()//还有元素吗?
    {
        return root==NULL;
    }
    int top()//堆顶是什么?
    {
        return empty()?-1:root->val;
    }
    node* pop()// 删堆顶,有返回值是方便回收
    {
        node* res=root;
        root=join(root->ch[0],root->ch[1]);
        return res;
    }
    void push(int x)//加点
    {
        now[++nodecnt]=new node(x);
        root=join(root,now[nodecnt]);
    }
    /*
    也可以定义
    void push(int x,int id)
    {
        now[id]=new node(x);
        root=join(root,now[id]);
    }
    以指定id,但注意不能与上面那种混用,否则会出现两个元素编号相同
    */
    node* erase(int x)// 删除编号为x的元素,有返回值是方便回收
    {
        if(now[x]==root)return pop();//特判没有父亲的情况
        bool k=now[x]->isr();
        now[x]->fa->ch[k]=join(now[x]->ch[0],now[x]->ch[1]);
        if(now[x]->fa->ch[k])now[x]->fa->ch[k]->fa=now[x]->fa;
        modup(now[x]->fa);
        return now[x];
    }
    void dkey(int x,int y)//调整编号为x元素值为y
    {
        now[x]=erase(x);// 回收!!
        now[x]->val=y;
        now[x]->dist=0;
        now[x]->fa=now[x]->ch[0]=now[x]->ch[1]=NULL;
        root=join(root,now[x]);
    }
};
posted @ 2025-02-21 19:51  ikusiad  阅读(41)  评论(0)    收藏  举报