可并堆/左偏树算法总结

可并堆

又名左偏树
顾名思义,可并堆是一个可以高效合并的堆结构。它可以在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);
                    }
                }
            }
		}
	} 
						
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值