左偏树学习笔记

一、前言

左偏树是一种可以在 O(logn) 内快速合并的堆式数据结构。
具体来说,
插入一个元素:O(logn)
查询最值:O(1)
删除最值:O(logn)
合并:O(logn)
减少一个元素的值:O(logn)
同时它可以持久化。

二、定义

  1. 外节点:左儿子或者右儿子为空的节点。
  2. 一个节点 x 的距离 disx 代表节点 x 到其所在的子树内的最近的外节点的距离。特别地,如果 x 是外节点,disx=0;如果 x 是空节点,disx=1

三、性质

  1. 左偏树满足小/大根堆的性质,即对于所有节点 x,满足 vxvlxvxvrx,或者对于所有节点 x,满足 vxvlxvxvrx
  2. 左偏树具有左偏性质,即对于任何一个节点 x,有 dislxdisrx

四、结论

  1. disx=disrx+1。这是因为根据定义,disx=min(dislx,disrx)+1 并且 dislxdisrx
  2. 距离为 n 的左偏树一共至少 2n+11 个节点,取等时它是满二叉树。这是因为根据定义,对于左偏树中的每一个节点 x 都满足 dislxdisrx=n1,否则 dislxdisrx,与性质矛盾。所以左子树的第 disrx+1=n 层的所有节点全部存在。递归地考虑,知道它至少是一棵满二叉树。所以至少 2n+11 个节点。
  3. 一棵有 n 个节点的左偏树距离至多为 log2(n+1)1。可以由结论 2 推得。

五、实现

我们以根节点的编号指代一棵左偏树。这里我们假定维护的是小根堆,即全部求的是节点权值的最小值。

  1. merge(x,y):合并两个以节点 xy 为根的左偏树,返回合并后的左偏树的根节点编号。这是左偏树最基础的操作。
    1. 首先,如果两棵树中有一棵为空,那么返回另外一棵树。
    2. 如果两棵树都非空,不妨假设 vx 小于 vy,否则交换 xy 以避免讨论。
    3. yx 的右子树 rx 合并。
    4. 返回时,如果回溯到节点 p 时, p 的右子节点 rp 的距离 disrp 大于 p 的左子节点 lp 的距离。 dislp,那么这不满足左偏树的左偏性质。所以我们交换 p 的左右子树即可。
    5. 最后,由于节点 p 的距离可能发生改变,我们要更新 p 的距离,即令 dispdisrp+1
      总结:这样就可以合并两棵左偏树了。由于所有遍历过的节点都被更新了,所以这种修改后的依然是一棵左偏树。时间复杂度是原先的 disx,是 O(logn) 级别的。
  2. 求最小值:根节点的值,时间复杂度 O(1)
  3. 删除最小值:删除根节点,合并根节点的左右子树,时间复杂度与合并两棵左偏树相同,为 O(logn)
  4. 插入新节点:容易知道一个点也是一棵左偏树,所以可以看做两棵左偏树的合并。时间复杂度也是 O(logn)
  5. 左偏树的建树:将所有节点作为左偏树放进先进先出的队列里,每次拿出队首的两棵左偏树,将它们合并后放到队尾。那么前 n2 次合并的是距离为 0 的左偏树,然后 n4 次合并的是距离为 1 的左偏树,……,然后 n2i 次合并的是距离为 i1 的左偏树。总复杂度为 n2×O(1)+n4×O(2)+n8×O(3)+=O(n)
  6. 在左偏树上删除一个指定节点(并非删除值为 x 的节点,而是删除编号为 x 的节点)。
    1. 首先,我们将 x 的左子树、右子树合并,得到 x 的子树新的根节点 p
    2. 如果 p 是全局的根节点,结束操作。
    3. 否则继续分类讨论,令 qp 的父亲节点。
      1. 新树的距离为 disp,如果满足 disq=disp+1 ,那么结束操作。
      2. 如果满足 disq>disp+1,那么 q 的距离应该更新为 disp+1,而且如果 pq 的左子节点,要交换 q 的左右子树。然后继续向上更新 q 的父亲节点。
      3. 如果满足 disq<disp+1,那么 q 的距离应该更新为 disp1,而且如果 pq 的右子节点,要交换 q 的左右子树。然后继续向上更新 q 的父亲节点。
        总结:这样就可以删除任何一个指定节点了。合并是 O(logn) 的,向上更新是 O(logn) 的,总复杂度 O(logn)
  7. 获得节点 x 所在的左偏树的根节点。直接向上跳,使用路径压缩即可。需要维护 rtx 的值,在合并和删除时要更新。这个操作最坏 O(logn)

合并:

点击查看代码
int merge(int x,int y){
	if(!x||!y)return x+y;
	if(val[x]<val[y])swap(x,y);
	r[x]=merge(r[x],y);
	if(dis[l[x]]<dis[r[x]])swap(l[x],r[x]);
	dis[x]=dis[r[x]]+1;
	return x;
}

删除非根节点:

点击查看代码
void pushup(ll x){
	if(!x)return;
	if(dis[x]!=dis[r[x]]+1){
		dis[x]=dis[r[x]]+1;
		pushup(fa[x]);
	}
}
ll merge(ll x,ll y){
	if(!x||!y)return x+y;
	if(val[x]<val[y])swap(x,y);
	fa[r[x]=merge(r[x],y)]=x;
	if(dis[l[x]]<dis[r[x]])swap(l[x],r[x]);
	pushup(x);
	return x;
}

六、例题

例 1.P3377 【模板】左偏树(可并堆)

点击查看代码
int n,m,v[100010],l[100010],r[100010],d[100010],dl[100010],rt[100010];
int merge(int x,int y){
	if(!x||!y)return x+y;
	if(v[x]>v[y]||x>y&&v[x]==v[y])swap(x,y);
	r[x]=merge(r[x],y);
	if(d[r[x]]>d[l[x]])swap(l[x],r[x]);
	d[x]=d[r[x]]+1;
	return x;
}
int find(int x){
	return x==rt[x]?x:rt[x]=find(rt[x]);
}
int main(){
	n=read();m=read();d[0]=-1;
	for(int i=1;i<=n;i++)v[i]=read(),rt[i]=i;
	for(int op,x,y;m;m--){
		op=read();x=read();
		if(op==1){
			y=read();
			if(dl[x]||dl[y])continue;
			x=find(x);y=find(y);
			if(x!=y)rt[x]=rt[y]=merge(x,y);
		}else{
			if(dl[x]){puts("-1");continue;}
			x=find(x);
			write(v[x]);putchar(10);
			rt[x]=rt[l[x]]=rt[r[x]]=merge(l[x],r[x]);
			dl[x]=1;d[x]=l[x]=r[x]=0;
		}
	}
	return 0;
}

例 2.P1456 Monkey King

点击查看代码
int merge(int x,int y){
	if(!x||!y)return x+y;
	if(val[x]<val[y])swap(x,y);
	r[x]=merge(r[x],y);
	if(dis[l[x]]<dis[r[x]])swap(l[x],r[x]);
	dis[x]=dis[r[x]]+1;
	return x;
}
int find(int x){
	return x==rt[x]?x:rt[x]=find(rt[x]);
}
int work(int x){
	val[x]/=2;
	int p=rt[l[x]]=rt[r[x]]=merge(l[x],r[x]);
	l[x]=r[x]=dis[x]=0;
	return rt[p]=rt[x]=merge(p,x);
}
int main(){
	while(cin>>n){
		for(int i=1;i<=n;i++){
			val[i]=read();
			rt[i]=i;
			l[i]=dis[i]=r[i]=0;
		}
		m=read();
		for(int x,y,p;m;m--){
			x=find(read());y=find(read());
			if(x==y)puts("-1");
			else{
				x=work(x);y=work(y);
				rt[x]=rt[y]=merge(rt[x],rt[y]);
				write(val[rt[x]]);
				putchar(10);
			}
		}
	}
	return 0;
}

例 3.P2713 罗马游戏

点击查看代码
int merge(int x,int y){
	if(!x||!y)return x+y;
	if(val[x]>val[y])swap(x,y);
	r[x]=merge(r[x],y);
	if(dis[l[x]]<dis[r[x]])swap(l[x],r[x]);
	dis[x]=dis[r[x]]+1;
	return x;
}
int find(int x){
	return x==rt[x]?x:rt[x]=find(rt[x]);
}
int main(){
	n=read();
	for(int i=1;i<=n;i++)val[rt[i]=i]=read();
	m=read();
	for(int i=1,x,y;i<=m;i++){
		char op;
		cin>>op;
		if(op=='M'){
			x=read();y=read();
			if(del[x]||del[y])continue;
			x=find(x);y=find(y);
			if(x!=y)rt[x]=rt[y]=merge(x,y);
		}else{
			x=read();
			if(del[x])putchar(48);
			else{
				x=find(x);
				write(val[x]);
				rt[x]=rt[l[x]]=rt[r[x]]=merge(l[x],r[x]);
				l[x]=r[x]=dis[x]=val[x]=0;
				del[x]=1;
			}
			putchar(10);
		}
	}
	return 0;
}

例 4.P1552 [APIO2012] 派遣

点击查看代码
int n,m,v[100010],b[100010],l[100010],r[100010],d[100010];
int sz[100010],rt[100010],c[100010],tag=0;
ll ans=0,sum[100010];
int merge(int x,int y){
	if(!x||!y)return x+y;
	if(c[x]<c[y])swap(x,y);
	r[x]=merge(r[x],y);
	if(d[r[x]]>d[l[x]])swap(l[x],r[x]);
	d[x]=d[r[x]]+1;
	return x;
}
int main(){
	n=read();m=read();
	for(int i=1;i<=n;i++){
		b[i]=read();c[i]=read();v[i]=read();
		rt[i]=i;sz[i]=1;sum[i]=c[i];
	}
	for(int i=n;i>=1;i--){
		while(sum[i]>m){
			sum[i]-=c[rt[i]];sz[i]--;
			rt[i]=merge(l[rt[i]],r[rt[i]]);
		}
		ans=max(ans,1ll*v[i]*sz[i]);
		sum[b[i]]+=sum[i];
		sz[b[i]]+=sz[i];
		rt[b[i]]=merge(rt[b[i]],rt[i]);
	}
	cout<<ans<<'\n';
	return 0;
}
### 七、参考文献、资料

题解 P3377 【【模板】左偏树(可并堆)】——雷哲涵
左偏树的特点及其应用——黄源河
左偏树——OI-WIKI

posted @   lrxQwQ  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示