//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

左偏树

左偏树

左偏树是一种可以让我们在 \(O(\log n )\) 的时间复杂度内进行合并的堆式数据结构。

为了方便以下的左偏树为小根堆来讨论。

定义

外结点:左儿子或者右儿子是空节点的结点。

距离:一个结点 \(x\) 的距离 \(dis[x]\) 定义为其子树中与结点 \(x\) 最近的外结点到 \(x\) 的距离。定义空节点的距离为 \(-1\)

性质

  • 左偏树具有堆性质,即若满足小根堆的性质,则对于每一个结点 \(x\),都有 \(w[x]\le w[ls[x]],w[x]\le w[rs[x]]\)。其中 \(w\) 为权值,\(ls,rs\) 为左儿子,右儿子。

  • 左偏树具有左偏的性质,即对于每一个点 \(x\),都有 \(dis[ls[x]]\ge dis[rs[x]]\)

基本结论

结点 \(x\) 的距离 \(dis[x]=dis[rs[x]]+1\),很明显上面说过的性质里面可以看出右儿子的距离更小,所以我们在计算当前结点的 \(dis\) 时应该用更小的 \(dis[rs]\)

距离为 \(n\) 的左偏树至少有 \(2^{n+1}-1\) 个结点,此时该左偏树的形态是一棵满二叉树。

操作

合并

左偏树的很多操作都是需要用到合并操作的。

我们用 merge(x,y) 来表示合并两棵以 \(x,y\) 为根节点的左偏树,其返回值就是合并之后的根节点的编号。

首先如果要是不考虑左偏的性质,假设我们合并的是小根堆:

  1. \(w[x]\le w[y]\),以 \(x\) 作为合并后的根节点;否则以 \(y\) 作为合并后的根节点。为了避免讨论,若有 \(w[x]>w[y]\) 我们就 swap 一下。

  2. \(y\)\(x\) 的其中一个儿子合并,用合并后的根节点代替与 \(y\) 合并的儿子的位置,并返回 \(x\)

  3. 重复以上操作,如果 \(x,y\) 有一个是 \(0\),就返回 \(x+y\),也就是返回不为 \(0\) 的结点的编号。

当然上述的方法在数据为一条链的时候会 T 飞,所以我们需要让他左右保持一个相对平衡的状态,这个时候我们就有了左偏树(当然平衡树也可以)。

由于我们前面说过左偏树中左儿子的距离大于右儿子的距离,我们每次将 \(y\)\(x\) 的右儿子合并,由于左偏树的树高为 \(\log n\) 的,所以单次合并的复杂度也为 \(O(\log n)\)

但是,两棵左偏树按照上述方法合并后,可能不再保持左偏树的左偏性质。在每次合并完之后,判断对结点 \(x\) 是否有 \(dis[ls[x]]\ge dis[rs[x]]\),若没有则交换 \(ls,rs\),并维护 \(x\) 的距离 \(dis[x]=dis[rs[x]]+1\),即可维护左偏树的左偏性质。

插入给定值

我们可以直接新建一个点,然后将他和左偏树合并就好。

求最小(大)值

由于满足堆的性质,所以我们直接返回堆顶的元素就好。

删除最小(大)值

也就是删除堆顶元素,直接合并根节点的左右儿子即可。

删除任意结点

inline void del(int x)
{
	int tmp=merge(ls[x],rs[x]),fu=fa[x];
	f[tmp]=fa[tmp]=fu;
	f[x]=fa[x]=tmp;
	ls[fu]==u?ls[fu]=tmp:rs[fu]=tmp;
	while(fu)
	{
		if(dis[ls[fu]]<dis[rs[fu]])swap(ls[fu],rs[fu]);
		if(dis[fu]==dis[rs[fu]]+1)return ;
		dis[fu]=dis[rs[fu]]+1;
		fu=fa[fu];
	}
}

这里 \(fa\) 是父节点。

我们和删除根节点一样先合并子树,然后存起来删除的点的父节点 \(fu\)

然后合并后的点的父节点和所属点先设为 \(fu\),然后我们把删除了的给调整成父节点和所在左偏树根节点为合并后的结点。

然后判断删除的点是父节点的左儿子还是右儿子然后用合并后的替换。

然后我们从父节点不断向上更新 \(dis\) 并用 swap 来维护左偏性质即可。

求给定结点所在的左偏树的根节点

我们可以开个数组 \(f\) 来记录父节点然后每次询问暴力跳,但是复杂度太高。

我们思考一下,我们可以像并查集一样采用路径压缩的办法来让这个复杂度变低。

在合并两个结点的时候,令 f[x]=f[y]=merge(x,y)

在删除左偏树中的最值时,我们令 f[ls[x]]=f[rs[x]]=f[x]=merge(x,y),因为 \(x\) 是之前左偏树的根节点,在路径压缩的时候可能有 \(f\) 的值等于 \(x\),所以 \(f[x]\) 也要指向删除后的根结点。

【模板】左偏树(可并堆) - 洛谷

code:


#include<bits/stdc++.h>
#define int long long
#define N 1000100
using namespace std;
int n,m,ls[N],rs[N],dis[N],f[N],vis[N];
struct sb{int id,w;bool operator<(sb x)const{return w==x.w?id<x.id:w<x.w;}}e[N];
inline int fid(int x){if(x==f[x])return x;return f[x]=fid(f[x]);}
inline int merge(int x,int y)
{
	if(!x||!y)return x+y;
	if(e[y]<e[x])swap(x,y);
	rs[x]=merge(rs[x],y);
	if(dis[ls[x]]<dis[rs[x]])swap(ls[x],rs[x]);
	dis[x]=dis[rs[x]]+1;
	return x;
}
signed main()
{
	dis[0]=-1;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	  cin>>e[i].w,f[i]=i,e[i].id=i;
	for(int i=1;i<=m;i++)
	{
		int op,x,y;
		cin>>op;
		if(op==1)
		{
			cin>>x>>y;
			if(vis[x]||vis[y])continue;
			int xx=fid(x),yy=fid(y);
			if(xx==yy)continue;
			f[xx]=f[yy]=merge(xx,yy);
		}
		else
		{
			cin>>x;
			if(vis[x]){puts("-1");continue;}
			int xx=fid(x);
			cout<<e[xx].w<<endl; 
			vis[xx]=1;
			f[ls[xx]]=f[rs[xx]]=f[xx]=merge(ls[xx],rs[xx]);
			ls[xx]=rs[xx]=dis[xx]=0;
		}
	}
	return 0;
}

参考:https://www.luogu.com.cn/blog/hsfzLZH1/solution-p3377

posted @ 2023-06-11 19:09  北烛青澜  阅读(156)  评论(5编辑  收藏  举报