世界真的很大
若要学可并堆的话,这道题是个比较裸的题了
可并堆的话左偏树算是比较常用的了
好写好调
看一下题先:
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;
}
嗯,就是这样