HDU 1512 浅谈可并堆即左偏树模板及并查集灵活应用

这里写图片描述
世界真的很大
若要学可并堆的话,这道题是个比较裸的题了
可并堆的话左偏树算是比较常用的了
好写好调
看一下题先:
description

有n只猴子,每只猴子有厉害值,一开始素不相识。
两只不熟的猴子相遇,它们会发生争执。然后,它们会邀请它们认识的最厉害的猴子决斗。决斗完这两只决斗的猴子的厉害值都会减半。决斗能促进友谊,这样这两拨素不相识的猴子就都认识了对方。
如果一只猴子不认识任何其他猴子,那么它就会亲自上阵。
每次给出两只猴子x,y,判断它们是否认识对方。若不认识,输出决斗后它们共同所在猴子群体中最厉害猴子的厉害值。

input

多组数据
每组数据先包含一个整数n,接下来n个整数表示每只猴子的厉害值,然后一个整数m,接下来m行,每行两个整数a,b,表示a猴子和b猴子会发生冲突

output

m行,每行一个整数,表示答案

在假设我们已经会了可并堆的情况下,来想想怎么做
每次查询两只猴子所在集合的最值,弹出,除以2,加入,再合并两个集合,输出新集合的最值
判断集合和合并集合很容易想到用并查集来处理,求最值也非常符合堆的性质
那这题就A了
只需要解决堆的合并的问题,接下来介绍一下这个可并堆,左偏树
有一篇论文

左偏树(Leftist Tree)是一种可并堆的实现。左偏树是一棵二叉树,它的节点除了和二叉树的节点一样具有左右子树指针( left, right )外,还有两个属性:键值和距离(dist)。键值上面已经说过,是用于比较节点的大小。距离则是如下定义的:
节点i称为外节点(external node),当且仅当节点i的左子树或右子树为空 ( left(i) = NULL或right(i) = NULL );节点i的距离(dist(i))是节点i到它的后代中,最近的外节点所经过的边数。特别的,如果节点i本身是外节点,则它的距离为0;而空节点的距离规定为-1 (dist(NULL) = -1)。在本文中,有时也提到一棵左偏树的距离,这指的是该树根节点的距离。

左偏树满足下面两条基本性质:

[性质1] 节点的键值小于或等于它的左右子节点的键值。
即key(i)≤key(parent(i)) 这条性质又叫堆性质。符合该性质的树是堆有序的(Heap-Ordered)。有了性质1,我们可以知道左偏树的根节点是整棵树的最小节点,于是我们可以在O(1) 的时间内完成取最小节点操作。

[性质2] 节点的左子节点的距离不小于右子节点的距离。
即dist(left(i))≥dist(right(i)) 这条性质称为左偏性质。性质2是为了使我们可以以更小的代价在优先队列的其它两个基本操作(插入节点、删除最小节点)进行后维持堆性质。在后面我们就会看到它的作用。

这两条性质是对每一个节点而言的,因此可以简单地从中得出,左偏树的左右子树都是左偏树。
由这两条性质,我们可以得出左偏树的定义:左偏树是具有左偏性质的堆有序二叉树。

[性质3] 节点的距离等于它的右子节点的距离加1。
即 dist( i ) = dist( right( i ) ) + 1 外节点的距离为0,由于性质2,它的右子节点必为空节点。为了满足性质3,故前面规定空节点的距离为-1。

我们的印象中,平衡树是具有非常小的深度的,这也意味着到达任何一个节点所经过的边数很少。左偏树并不是为了快速访问所有的节点而设计的,它的目的是快速访问最小节点以及在对树修改后快速的恢复堆性质。从图中我们可以看到它并不平衡,由于性质2的缘故,它的结构偏向左侧,不过距离的概念和树的深度并不同,左偏树并不意味着左子树的节点数或是深度一定大于右子树。

这些性质对接下来的操作思考是很有用的

左偏树的操作

1.左偏树的合并
Merge( ) 把A,B两棵左偏树合并,返回一棵新的左偏树C,包含A和B中的所有元素。在本文中,一棵左偏树用它的根节点的指针表示。

在合并操作中,最简单的情况是其中一棵树为空(也就是,该树根节点指针为NULL)。这时我们只须要返回另一棵树。

若A和B都非空,我们假设A的根节点小于等于B的根节点(否则交换A,B),把A的根节点作为新树C的根节点,剩下的事就是合并A的右子树right(A) 和B了。

合并了right(A) 和B之后,right(A) 的距离可能会变大,当right(A) 的距离大于left(A) 的距离时,左偏树的性质2会被破坏。在这种情况下,我们只须要交换left(A) 和right(A)。

最后,由于right(A) 的距离可能发生改变,我们必须更新A的距离:
dist(A) ← dist(right(A)) + 1
不难验证,经这样合并后的树C符合性质1和性质2,因此是一棵左偏树。至此左偏树的合并就完成了。

代码:

Lefeap* merge(Lefeap* a,Lefeap* b)
{
    if(a==0) return b;
    if(b==0) return a;
    if(a->val<b->val) 
        swap(a,b);
    a->rs=merge(a->rs,b);
    int x=-1,y=-1;
    if(a->rs) x=a->rs->dis;
    if(a->ls) y=a->ls->dis;
    if(x>y) 
        swap(a->rs,a->ls);
    if(a->rs==0) 
        a->dis=0;
    else a->dis=a->rs->dis+1;
    return a;
}

2.左偏树的插入
单节点的树一定是左偏树,因此向左偏树插入一个节点可以看作是对两棵左偏树的合并。下面是插入新节点的代码:

void insert(Lefeap *&a,int x)
{
    Lefeap *B=newnode(x);
    a=merge(a,B);
}

3.左偏树的弹出
左偏树具有堆的性质,所以弹出时直接返回最小(大)元素的值,也就是根节点,然后合并其左右子树就行了,代码:

int pop(Lefeap *&nd)
{
    int tmp=nd->val;
    nd=merge(nd->ls,nd->rs);
    return tmp;
}

完整代码:

#include<stdio.h>
#include<algorithm>
#include<cstring>
using namespace std;

struct Lefeap
{
    int val,dis;
    Lefeap *ls,*rs;
}pool[1000010],*tail=pool,*root[100010];

int fa[100010],n,m;

Lefeap* newnode(int w)
{
    Lefeap *nd=++tail;
    nd->val=w;
    nd->dis=0;
    nd->ls=nd->rs=0;
    return nd;
}

void init()
{
    memset(fa,0,sizeof(fa));
    memset(root,0,sizeof(root));
    tail=pool+0;
}

int getfather(int x)
{
    if(x==fa[x]) return x;
    return fa[x]=getfather(fa[x]);
}

Lefeap* merge(Lefeap* a,Lefeap* b)
{
    if(a==0) return b;
    if(b==0) return a;
    if(a->val<b->val) 
        swap(a,b);
    a->rs=merge(a->rs,b);
    int x=-1,y=-1;
    if(a->rs) x=a->rs->dis;
    if(a->ls) y=a->ls->dis;
    if(x>y) 
        swap(a->rs,a->ls);
    if(a->rs==0) 
        a->dis=0;
    else a->dis=a->rs->dis+1;
    return a;
}

void insert(Lefeap *&a,int x)
{
    Lefeap *B=newnode(x);
    a=merge(a,B);
}

int pop(Lefeap *&nd)
{
    int tmp=nd->val;
    nd=merge(nd->ls,nd->rs);
    return tmp;
}

int main()
{
    while(scanf("%d",&n)!=EOF)
    {
        init();
        for(int i=1;i<=n;i++)
        {
            int w;
            scanf("%d",&w);
            root[i]=newnode(w);
            fa[i]=i;
        }
        scanf("%d",&m);
        while(m--)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            int x=getfather(a);
            int y=getfather(b);
            if(x==y)
            {
                printf("-1\n");
                continue ;
            }
            int tmp1=pop(root[x]);
            int tmp2=pop(root[y]);
            insert(root[x],tmp1/2);
            insert(root[y],tmp2/2);
            fa[y]=x;
            root[x]=merge(root[x],root[y]);
            printf("%d\n",root[x]->val);
        }
    }
    return 0;
}

嗯,就是这样

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值