可并堆
又名左偏树
顾名思义,可并堆是一个可以高效合并的堆结构。它可以在O(logn)的复杂度下实现删除,查找最小值,合并的操作。但注意可并堆的复杂度很假…因为其树高可以达到O(n),所以可能会被卡
逻辑结构
我们定义左右儿子任一为空的节点为外节点,空节点也是一个外节点。
定义某节点dist的值为其子树中外节点到它最近的距离,定义空节点的dist值为-1,显然我们能知道
- 外节点的dist值为0
左偏树是这样的一棵树:满足堆的性质,同时任一节点,其左节点的dist大于其右节点的dist,那么我们显然可得
- dist(x)=dist(x.rson)+1
需要注意的是,dist并非树的深度,一条向左的链也是左偏树,但dist是logn左右的量。
操作实现
核心操作——合并
左偏树的合并操作是核心操作,我们先不管左偏树的左右儿子dist要求,我们考虑合并xy,避免讨论,我们让x<y。满足堆的性质,我们让y和x右儿子合并作为x的新右儿子,递归的处理即可,为了满足dist要求,我们在处理之后检查左右儿子的dist,不满足就互换即可。我们注意到每次处理可将两个堆之一的dist减一,dist为logn的量,那么每次合并操作是O(logn)的
插入操作
既然合并的复杂度十分优秀,那么我们就要利用这个性质。显然,单个节点也是左偏树,因此插入操作当作合并即可
查找某值所在堆顶
其实如果只有一个堆的话,这个操作就是查找最小值,但选择可并堆是因为其合并优秀复杂度,那么也就是说需要维护多个堆,那么在多个堆中查找某值对应的堆的最小值就有点麻烦了。
因为可并堆的树高可以到达O(n),直接暴力的找肯定会被卡,我们使用类似并查集的方法维护每个节点的fa值就好了,运用路径压缩的方法可以大幅降低复杂度。
删除最小值
同理,我们将堆顶删除,之后合并左右儿子即可
注意因为我们使用路径压缩实现高效查找父节点,所以在删除x节点时,可能有很多个节点的fa指向x,为了不让这些节点失去父亲,我们需要在每次删除节点x后将x的fa指向他左右儿子合并后的新根节点
删除任意节点
待完善…https://www.luogu.com.cn/problem/P3273
先删除该点,合并其左右子树,作为替换该点的新子树。之后从该点向上遍历,重新维护这一条链上的dist。复杂度是O(logn)
代码实现
/*左偏树
实现o(logn)的删除,查询和合并
46行操作为题目要求,删点时删去出现最早的,可删
注意merge x,y操作后需要在主函数更新xy的祖先,merge函数内未实现
*/
#include<cstdio>
#include<iostream>
#include<iomanip>
#include<map>
#include<unordered_map>
#include<string>
#include<queue>
#include<stack>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<chrono>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define endl "\n"
#define int long long
//#define double long double
using namespace std;
typedef long long ll;
const int maxn=200005;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
int n,m,k;
struct custom_hash {static uint64_t splitmix64(uint64_t x) {x += 0x9e3779b97f4a7c15;x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;x = (x ^ (x >> 27)) * 0x94d049bb133111eb;return x ^ (x >> 31);}size_t operator()(uint64_t x) const {static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();return splitmix64(x + FIXED_RANDOM);}};
struct left_tree{
int dis,val;
int fa,lson,rson;
}lt[maxn];
void init(){
lt[0].dis=-1;
for(int i=0;i<maxn;i++)
lt[i].fa=i;
}
inline int find(int x){
return lt[x].fa==x?x:lt[x].fa=find(lt[x].fa);
}
//递归地x的右儿子和y
int merge(int x,int y){
if(!x||!y) return x+y;
if(lt[x].val>lt[y].val) swap(x,y);//保证x<y
else if(lt[x].val==lt[y].val&&x>y) swap(x,y);
int ls=lt[x].lson,rs=lt[x].rson;
rs=merge(rs,y);
if(lt[ls].dis<lt[rs].dis) swap(ls,rs);
lt[ls].fa=lt[rs].fa=lt[x].fa=x;
lt[x].dis=lt[rs].dis+1;
lt[x].lson=ls,lt[x].rson=rs;
return x;
}
void pop(int x){
lt[x].val=-1;//拆除自己
lt[lt[x].lson].fa=lt[x].lson;//拆除左右儿子
lt[lt[x].rson].fa=lt[x].rson;
lt[x].fa=merge(lt[x].lson,lt[x].rson);//合并左右儿子
//为保证路径压缩后指向x的节点在x删除后不指错,fa[x]也需要更改
}
signed main(){
IOS
#ifndef ONLINE_JUDGE
freopen("IO\\in.txt","r",stdin);
freopen("IO\\out.txt","w",stdout);
#endif
int tn=1;
while(tn--){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>lt[i].val;
}
init();
while(m--){
int op;
cin>>op;
if(op==1){
int x,y;
cin>>x>>y;
if(lt[x].val==-1||lt[y].val==-1) continue;
x=find(x),y=find(y);
if(x!=y) lt[x].fa=lt[y].fa=merge(x,y);//记得要更新fa
}
else{
int t;
cin>>t;
if(lt[t].val==-1)cout<<lt[t].val<<endl;
else{
t=find(t);
cout<<lt[t].val<<endl;
pop(t);
}
}
}
}
}