Leftist Tree(左偏树)学习笔记

基本概念

左偏树。顾名思义,就是向左偏的二叉树。它满足堆性质与左偏性质。

外结点与距离

我们定义不足两个儿子的节点为外结点。\(dist_x\) 称为 \(x\) 的距离,且有 \(dist_x = \min\{ dist _ {lc}, dist _ {rc}\}+1\)。这是递归定义,其实 \(dist _ x\) 就是 \(x\) 的子树中与 \(x\) 距离最近的外结点与 \(x\) 的距离。特殊的,定义空节点的距离为 \(-1\)

堆性质与左偏性质

众所周知,堆性质就是 \(\forall x|val _ x \le val _ {lc}\ ,val _ x \le val _ {rc}\)
而左偏性质就是 \(\forall x|dist _ {lc} \ge dist _ {rc}\)

基本结论

  1. \(\forall x|dist _ x = dist _ {rc} + 1\)

  2. 距离为 \(n\) 的左偏树至少有 \(2 ^ {n+1} - 1\) 个结点,并且是一颗满二叉树。

  3. 根节点的距离 \(dist _ {root} \le \log{n}\)

基本操作

合并 merge(x,y)

即合并以 \(x ,y\) 为根的两棵左偏树,并返回合并后根节点的编号。
这是左偏树最基本的操作。
在合并时我们需要保持堆性质与左偏性质。
首先,merge(x,y) 操作默认令 \(x\) 为合并后的根节点(当 \(x ,y\) 皆不为空节点时)。
为了保持堆性质,我们就得在 \(val _ x > val _ y\) 时将 \(x ,y\) 交换,以此来保持堆性质。
由于左偏性质的存在,将 \(y\)\(x\) 的右儿子合并比将 \(y\)\(x\) 的左儿子合并是更优的,这样可以使得合并后的树的深度更小,也就使每次操作的时间复杂度稳定在 \(\log{n}\)
所以只需要递归合并 \(x\) 的右儿子与 \(y\) 即可。
但是这就导致了一个问题:\(x\) 的右儿子与 \(y\) 合并后可能导致在 \(x\) 的儿子节点中出现 \(dist _ {lc} < dist _ {rc}\) 的情况,这就会违背左偏性质,此时就需要将 \(lc , rc\) 交换。
最后维护一下 \(dist _ x\) 即可(\({dist} _ x = {dist} _ {lc} +1\))。
一般来说,merge(x,y) 后的根节点是 \(x\),但当 \(x ,y\) 中有一个是空节点时应返回不是空节点的那个。

int merge(int x,int y){
	if(!x||!y) return x+y;//就是将 if(!x) return y;if(!y) return x; 简写了一下
	if(t[x].val>t[y].val) swap(x,y);//维护堆性质,也可以将“>”换成“<”以此来换成大根堆
	t[x].ch[1]=merge(t[x].ch[1],y);//依照左偏性质,与右儿子合并更优
	if(t[t[x].ch[0]].dis<t[t[x].ch[1]].dis) swap(t[x].ch[0],t[x].ch[1]);//维护左偏性质
	t[x].dis=t[t[x].ch[1]].dis+1;//更新x的距离
	return x;
}

在这里提一嘴,大多数左偏树的题目都需要查找根节点,此时我们就只需要多维护一个并查集数组 \(rt _ x\) 即可。
在合并时也应同步更新 \(rt\) 数组,显然可以直接在维护完一系列性质之后用 t[ls].rt=t[rs].rt=t[x].rt=x; 更新并查集数组(在维护距离前后),这样在每一次合并后这个左偏树上每一个节点的 \(rt\) 都会被重置,重新变成一个多层的树,并且 merge(x,y) 合并后需要令 t[x].rt=t[y].rt=merge(x,y);
但这样一来就可以查询任意一个节点所在的左偏树的根。

其他基本操作

插入一个数

只需要新建一个权值等于插入树的节点,将它与左偏树合并即可,时间复杂度 \(\mathcal{O}(\log _ 2 {n})\)

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

前面已经提到了维护一个并查集数组 \(rt\),所以我们可以用一个 fnd(x) 函数来查询,和并查集是一样的,为了保证时间复杂度,我们需要用到路径压缩。

int fnd(int x){
	return ((t[x].rt==x)?x:t[x].rt=fnd(t[x].rt));//路径压缩并查集
}

求最小/最大值

根据左偏树的堆性质,左偏树上根节点的值就是最小/最大值。

删除一个点

只要合并它的左右儿子,再删除这个节点的信息即可。
如果维护了并查集数组就得在删除节点后进行更新,令 t[ls].rt=ls;t[rs].rt=rs;t[x].rt=merge(ls,rs); 即可。
所以被删除的节点就不能再用了,如果要还原就得新建一个节点。

几道例题

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

作为一道模板题,此题的题面还是很简洁的,总的来说只要按照题面上所说的步骤模拟即可。

Code:

#include<iostream>
using namespace std;
const int N(1e5+5);
int n,m;
bool ok[N];
namespace leftist_tree{
	int ch[N][2],val[N],rt[N],dis[N];
	inline int merge(int x,int y){
		if(!x||!y) return x+y;
		if(val[x]>val[y]||(val[x]==val[y]&&x>y)) swap(x,y);
		ch[x][1]=merge(ch[x][1],y);
		if(dis[ch[x][0]]<dis[ch[x][1]]) swap(ch[x][0],ch[x][1]);
		rt[ch[x][0]]=rt[ch[x][1]]=rt[x]=x;
		dis[x]=dis[ch[x][1]]+1;
		return x;
	}
	inline int fnd(int x){
		return ((rt[x]==x)?x:rt[x]=fnd(rt[x]));
	}
	inline void pop(int x){
		val[x]=-1;
		rt[ch[x][0]]=ch[x][0];rt[ch[x][1]]=ch[x][1];
		rt[x]=merge(ch[x][0],ch[x][1]);
	}
}
using namespace leftist_tree;
int main(){
	#ifdef ytxy
	freopen("in.txt","r",stdin);
	#endif
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	dis[0]=-1;
	for(int i=1;i<=n;i++)
		cin>>val[i],rt[i]=i;
	while(m--){
		int op,x,y;
		cin>>op>>x;
		if(op==1){
			cin>>y;
			if(val[x]==-1||val[y]==-1) continue;
			int fx=fnd(x),fy=fnd(y);
			if(fx!=fy) rt[fx]=rt[fy]=merge(fx,fy);
		}
		else{
			if(val[x]==-1) cout<<-1<<'\n';
			else cout<<val[fnd(x)]<<'\n',pop(fnd(x));
		}
	}
}

P1456 Monkey King

也是一道左偏树的模板题,初步读题可以知道这一题其实就是要求我们编写一个数据结构,资瓷取出最大值、加入一个数、合并两个数值所在的数据结构。

很明显可以用左偏树来写。

仍然是模拟题意即可。

Code:

#include<iostream>
using namespace std;
const int N(1e5+5);
int n,m;
namespace Leftist_Tree{
	struct {
		int ch[2],val,rt,dis;
	} t[N];
	#define ls t[x].ch[0]
	#define rs t[x].ch[1]
	int fnd(int x){
		return ((t[x].rt==x)?x:t[x].rt=fnd(t[x].rt));
	}
	int merge(int x,int y){
		if(!x||!y) return x+y;
		if(t[x].val<t[y].val) swap(x,y);
		rs=merge(rs,y);
		if(t[ls].dis<t[rs].dis) swap(ls,rs);
		t[ls].rt=t[rs].rt=t[x].rt=x;
		t[x].dis=t[rs].dis+1;
		return x;
	}
}
using namespace Leftist_Tree;
void fight(int x){
	t[x].val>>=1;
	int root;
	root=merge(ls,rs);
	ls=rs=t[x].dis=0;
	t[root].rt=t[x].rt=merge(x,root);
}
int main(){
	#ifdef ytxy
	freopen("in.txt","r",stdin);
	#endif
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	while(cin>>n){
		t[0].dis=-1;
		for(int i=1;i<=n;i++)
			cin>>t[i].val,t[i].rt=i,t[i].ch[0]=t[i].ch[1]=0;
		cin>>m;
		while(m--){
			int x,y;
			cin>>x>>y;
			int fx=fnd(x),fy=fnd(y);
			if(fx==fy){
				cout<<-1<<'\n';
				continue;
			}
			fight(fx);
			fight(fy);
			t[fx].rt=t[fy].rt=merge(t[fx].rt,t[fy].rt);
			cout<<t[t[fx].rt].val<<'\n';
		}
	}
}
posted @ 2022-11-11 13:01  JR_ytxy  阅读(42)  评论(0)    收藏  举报