[note]左偏树(可并堆)
左偏树(可并堆)https://www.luogu.org/problemnew/show/P3377
题目描述
一开始有N个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:
操作1: 1 x y 将第x个数和第y个数所在的小根堆合并(若第x或第y个数已经被删除或第x和第y个数在用一个堆内,则无视此操作)
操作2: 2 x 输出第x个数所在的堆最小数,并将其删除(若第x个数已经被删除,则输出-1并无视删除操作)
输入格式:
第一行包含两个正整数N、M,分别表示一开始小根堆的个数和接下来操作的个数。
第二行包含N个正整数,其中第i个正整数表示第i个小根堆初始时包含且仅包含的数。
接下来M行每行2个或3个正整数,表示一条操作,格式如下:
操作1 : 1 x y
操作2 : 2 x
输出格式:
输出包含若干行整数,分别依次对应每一个操作2所得的结果。
输入样例:
5 5
1 5 4 2 3
1 1 5
1 2 5
2 2
1 4 2
2 2
输出样例:
1
2
说明
当堆里有多个最小值时,优先删除原序列的靠前的,否则会影响后续操作1导致WA。
时空限制:1000ms,128M
数据规模:
对于30%的数据:N<=10,M<=10
对于70%的数据:N<=1000,M<=1000
对于100%的数据:N<=100000,M<=100000
样例说明:
初始状态下,五个小根堆分别为:{1}、{5}、{4}、{2}、{3}。
第一次操作,将第1个数所在的小根堆与第5个数所在的小根堆合并,故变为四个小根堆:{1,3}、{5}、{4}、{2}。
第二次操作,将第2个数所在的小根堆与第5个数所在的小根堆合并,故变为三个小根堆:{1,3,5}、{4}、{2}。
第三次操作,将第2个数所在的小根堆的最小值输出并删除,故输出1,第一个数被删除,三个小根堆为:{3,5}、{4}、{2}。
第四次操作,将第4个数所在的小根堆与第2个数所在的小根堆合并,故变为两个小根堆:{2,3,5}、{4}。
第五次操作,将第2个数所在的小根堆的最小值输出并删除,故输出2,第四个数被删除,两个小根堆为:{3,5}、{4}。
故输出依次为1、2。
给大家介绍一篇左偏树讲解
以下摘自国集2005论文集
左偏树(Leftist Tree)
是一种可并堆的实现。左偏树是一棵二叉树,它的节点除了和二叉树的节点一样具有左右子树指针外,还有两个属性:点权(键值)和距离(dist).
左偏树满足下面两条基本性质:
[性质1] 节点的键值小于或等于它的左右子节点的键值.
这条性质又叫堆性质。符合该性质的树是堆有序的。
有了性质1,我们可以知道左偏树的根节点是整棵树的最小节点,于是我们可以在O(1) 的时间内完成取最小节点操作。
[性质2] 节点的左子节点的距离不小于右子节点的距离.
即dist(left(i))≥dist(right(i)) 这条性质称为左偏性质。
性质2是为了使我们可以以更小的代价在优先队列的其它两个基本操作(插入节点、删除最小节点)进行后维持堆性质。
这两条性质是对每一个节点而言的,因此可以简单地从中得出,左偏树的左右子树都是左偏树。
由这两条性质,我们可以得出左偏树的定义:左偏树是具有左偏性质的堆有序二叉树。
习惯上常用根节点序号表示左偏树
#define RG register
#include<cstdio>
#include<iostream>
using namespace std;
const int N=1e5+5;
inline int read()
{
RG int x=0,w=1;RG char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
return x*w;
}
int n,m;
int num[N],ls[N],rs[N],root[N],dist[N];//num[]为每个点的值,ls[]和rs[]为左右儿子,root[]表示该节点所在堆的堆顶编号(左偏树根节点),dist[i]表示节点i节点i到它的后代中最近的外节点所经过的边数[某节点称为外节点,当且仅当该节点的左子树或右子树为空].
bool del[N];//判断点是否被删除
int find(int x){return x==root[x]?x:root[x]=find(root[x]);}//并查集找根节点
int Merge(int a,int b)
{
if(!a||!b)return a+b;//若某一子树为空,返回另一棵子树
if(num[a]>num[b]||(num[a]==num[b]&&a>b))swap(a,b);//维护小根堆[权值相同,序号优先]
rs[a]=Merge(rs[a],b);//每次将a的右子树与b合并
if(dist[ls[a]]<dist[rs[b]])swap(ls[a],rs[a]);//交换左右儿子[保证左偏]
dist[a]=dist[rs[a]]+1;
return a;
}
int main()
{
n=read();
m=read();
for(int i=1;i<=n;i++)num[i]=read(),root[i]=i;
while(m--)
{
int f=read();
if(f==1)
{
int a=read(),b=read();
int x=find(a),y=find(b);
if(x==y||del[a]||del[b])continue;
root[x]=root[y]=Merge(x,y);//修改根节点
}
else
{
int x=read();
if(del[x]){printf("-1\n");continue;}
x=find(x);
printf("%d\n",num[x]);
del[x]=true;
root[x]=Merge(ls[x],rs[x]);//修改根节点
if(ls[x]==root[x])root[ls[x]]=ls[x];//将根节点的根置为自己[防止find()递归出错]
if(rs[x]==root[x])root[rs[x]]=rs[x];//将根节点的根置为自己
ls[x]=rs[x]=0;//清空被删除点的左右儿子
}
}
return 0;
}