可并堆之左偏树

左偏树

可并堆,就是支持合并,并且满足堆性质的二叉树,先不管如何来实现合并,假设我们已经维护好了这样的数据结构,如何来实现堆应该有的操作:
插入:插入一个节点可以看成把一个单个节点的堆(单点显然满足堆性质)和堆进行合并
删除根:可以看成把根的左子树和右子树合并
所以,可并堆只需要一个操作:合并,就可以实现所有堆应该有的操作。
左偏树:
左偏树最重要的性质正如它的名字——它是左偏的,也就是说,左偏树的任意节点都满足它的左子树的节点个数小于右子树的节点个数,这个性质有什么用呢?如果我们从根节点开始,不停的递归右子树,次数一定小于log2(n),因为每次递归右子树,由于右子树的节点个数一定小于左子树,可以看成节点个数/2。
这个性质对于合并就有很大的好处,由于这个性质,合并时只要不停的用右子树去合并,单次合并的复杂度就可以看成是log2(n)的。
合并:
合并时,考虑将树x和树y合并,显然,合并后的树的根节点一定是两个树的根节点中值较小的那个,为了方便操作,默认将树x的根节点作为根,也就是说,每次都把y与x的右子树合并,y与x的右子树合并后的根节点就是树x的右儿子,还要满足左偏性质,如果合并后右子树的节点个数大于左子树的节点个数,就交换左右子树,最后更新当前子树的大小。
代码如下:

int merge(int x,int y){
    if((!x)||(!y))return x|y;
    if(hep[y]<hep[x])swap(x,y);
    int son=merge(hep[x].r,y);
    hep[x].r=son;
    if(hep[hep[x].r].num>hep[hep[x].l].num)swap(hep[x].r,hep[x].l);
    hep[x].num=hep[hep[x].l].num+1+hep[hep[x].r].num;
    return x;
}

下面来看一道题目(摘自洛谷):
一开始有N个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:

操作1: 1 x y 将第x个数和第y个数所在的小根堆合并(若第x或第y个数已经被删除或第x和第y个数在用一个堆内,则无视此操作)

操作2: 2 x 输出第x个数所在的堆最小数,并将其删除(若第x个数已经被删除,则输出-1并无视删除操作)

这题显然就是左偏树的模板题,但是有一点需要注意,在实际运用中,左偏树还需要维护每个节点所在的堆的堆顶,马上就会想到并查集来维护,但是,删除操作怎么办?并查集并不天然支持删除某个节点,好在这题只需要删除堆顶就可以,显然,合并操作时只要将需要合并的两个堆的fa中将要作为儿子节点的fa修改为最终的根节点就可以。
但是,删除一个节点要如何办到?由于删除一个根节点后可能成为根节点的只有两个——它的两个儿子,所以,只要把会成为新的根节点的fa更新为它自己,再把原来的根节点的fa修改为新的根节点即可。
代码如下:

                int k=merge(hep[x].l,hep[x].r);
                fa[x]=k;fa[k]=k;
                vis[x]=0;

其中的vis是用来标记某个节点是否被删除。
本题代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 500006
using namespace std;
struct data{
    int l,r,s,num,id;
    bool operator <(const data&b)const{
        return s<b.s||(s==b.s&&id<b.id);
    }
}hep[maxn];
int n,m,fa[maxn];
bool vis[maxn];
int merge(int x,int y){
    if((!x)||(!y))return x|y;
    if(hep[y]<hep[x])swap(x,y);
    int son=merge(hep[x].r,y);
    hep[x].r=son;
    if(hep[hep[x].r].num>hep[hep[x].l].num)swap(hep[x].r,hep[x].l);
    hep[x].num=hep[hep[x].l].num+1+hep[hep[x].r].num;
    return x;
}
int get(int x){return fa[x]==x?x:fa[x]=get(fa[x]);}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&hep[i].s),hep[i].num=1,hep[i].id=i,fa[i]=i;
    memset(vis,1,sizeof(vis));
    for(int i=1,p,x,y;i<=m;i++){
        scanf("%d",&p);
        if(p==1){
            scanf("%d%d",&x,&y);
            if((!vis[x])||(!vis[y]))continue;
            x=get(x);y=get(y);
            if(hep[x]<hep[y])fa[y]=x;
                        else fa[x]=y;
            if(x!=y)merge(x,y);
        }else{
            scanf("%d",&x);
            if(vis[x]){
                x=get(x);
                printf("%d\n",hep[get(x)].s);
                int k=merge(hep[x].l,hep[x].r);
                fa[x]=k;fa[k]=k;
                vis[x]=0;
            }else printf("-1\n");
        }
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值