左偏树


左偏树是可并堆的一种(除了左偏树别的我不会)。可并堆具有堆的性质,可以快速合并。
首先我们回想一些我们比较熟悉的东西。

1.优先队列
我们通常用优先队列堆优化(小根堆)时是这样:

priority_queue<int,vector<pair<int,int> > ,greater<pair<int,int> > >q;

我们会得到按照加入的\(dis\)\((first)\)从小到大排好序的队列。其中,\(dis\)就是键值。

2.二叉堆
从二叉堆的结构说起,它是一棵二叉树,并且是完全二叉树,每个结点中存有一个元素(或者说,有个权值)。

堆性质:父亲的权值不小于儿子的权值(大根堆)。同样的,还有小根堆。 ——by oi wiki

其实我感觉可并堆和二叉堆就是多了个可以快速合并(

左偏树的定义

左偏树是一种可并堆的实现。左偏树是一棵二叉树,它的节点除了和二叉树的节点一样具有左右子树指针\(( left, right )\)外,还有两个属性:键值\((key)\)和距离\((dist)\)。键值用于比较节点的大小。

dist的定义和性质:

当节点i的左子树或右子树为空时,称节点i称为外节点\((external node)\)

节点\(i\)的距离\((dist(i))\)是节点i到它的后代中,最近的外节点所经过的边数。

特别的,如果节点\(i\)本身是外节点,则它的距离为\(0\)

而空节点的距离规定为\(-1\) 。有时提到一棵左偏树的距离,这指的是该树根节点的距离。

看一个左偏树的图\(\Downarrow\)

外节点\(\Downarrow\)

基本性质

val表示key,即键值

左偏:

每个节点左儿子的dist都大于等于右儿子的dist。

因此,左偏树每个节点的dist都等于其右儿子的dist加一。
但是,dist不是深度,一条向左的链也是左偏树。

性质1:
满足堆的性质,对于任意一点p,\(val[p]\ge(\le)val[l],val[r]\)

性质2:
\(dist[x]=dist[r]+1\),也就是说,任意节点的距离等于其右子树的距离+1。

左偏性质以及外节点的距离为0可以得出。

定理及引理

定理:

对于一个有\(n\)个节点的左偏树,最大距离不超过\(\log(n+1)−1\),即\(max\{dist\}\leq \log(n+1)-1\)

证明: 设\(max\{dist\}=k\),那么显然节点数最少的左偏树是一棵完全二叉树,此时节点数为\(2^{k+1}-1\),故\(n\geq 2^{k+1}-1\),移项得:\(k\leq \log(n+1)-1\)

引理:

如果一棵左偏树的距离为k,则这颗左偏树节点数最少为\(2^{k+1}−1\)

证明: 因为左偏树的左子树距离一定大于等于右子树的距离,并且左偏树的距离等于右子树距离加一,那么一颗左偏树的距离一定且要求节点数最少时,节点数最少时任意节点的左子树大小等于右子树大小,满足这样性质的二叉树就是完全二叉树。

这个大家试着理解一下,暂时不理解也没关系。后面的内容没怎么提到。

几个简单的基本操作

合并(merge)

合并两个堆时,由于要满足堆性质,先取值较小(或大)的那个根作为合并后堆的根节点,然后将这个根的左儿子作为合并后堆的左儿子,递归地合并其右儿子与另一个堆,作为合并后的堆的右儿子。

为了满足左偏性质,合并后若左儿子的\(dist\)小于右儿子的\(dist\),就交换两个儿子。

合并过程

代码实现

int merge(int x,int y){
	if(!x||!y) return x+y;//若有一堆为空 则返回另一个
	if(t[x].val>t[y].val||t[x].val==t[y].val&&x>y)swap(x,y);//满足堆性质 取较小值 若值相同 使x编号较小 直接写成if(t[x].val>t[y].val)也行
	//x的做儿子作为合并后堆的做儿子,递归合并右儿子和右子树
	t[x].r=merge(t[x].r,y);
	t[t[x].r].fa=x;//更新父亲
	if(t[t[x].l].dist<t[t[x].r].dist)swap(t[x].l,t[x].r);//左偏 左儿子大于右儿子
	t[x].dist=t[t[x].r].dist+1;//跟新根值
	return x;
}

主函数中

	int X=Find(x),Y=Find(y);
	if(X!=Y)
		t[X].fa=t[Y].fa=merge(X,Y);

插入节点

把单点视为一个堆,按上面的方法合并即可。

代码实现

t[x].fa=t[y].fa=merge(x,y);//x是插入的点,y是要插入的堆

删除根(pop)

把根的\(val=-1\)(标记为被删除),合并根的左右儿子即可。

代码实现

void pop(int x){
	t[x].val=-1;//标为被删除
	t[t[x].l].fa=t[x].l;//更新两孩子父亲为孩子本身,相当于把x删除
	t[t[x].r].fa=t[x].r;
	t[x].fa=merge(t[x].l,t[x].r);//更新x的孩子的父节点,把重新合并后的根赋值给x的父节点
}

【模板】左偏树(可并堆

分析

题目说了是个模板

用到的只有基本的合并和删除操作。

是个小根堆。

没啥好说的直接看代码吧((

代码实现

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,op,x,y;
struct node{
	int l,r,dist,val,fa;
}t[N];
int Find(int x){//并查集查询操作
	if(t[x].fa==x)return x;
	t[x].fa=Find(t[x].fa);
	return t[x].fa;
}
int merge(int x,int y){
	if(!x||!y) return x+y;//若有一堆为空 则返回另一个
	if(t[x].val>t[y].val||t[x].val==t[y].val&&x>y)swap(x,y);//满足堆性质 取较小值 若值相同 使x编号较小
	//x的做儿子作为合并后堆的做儿子,递归合并右儿子和右子树
	t[x].r=merge(t[x].r,y);
	t[t[x].r].fa=x;//更新父亲
	if(t[t[x].l].dist<t[t[x].r].dist)swap(t[x].l,t[x].r);//左偏 左儿子大于右儿子
	t[x].dist=t[t[x].r].dist+1;//跟新根值
	return x;
}
void pop(int x){
	t[x].val=-1;//标为被删除
	t[t[x].l].fa=t[x].l;//更新两孩子父亲为孩子本身,相当于把x删除
	t[t[x].r].fa=t[x].r;
	t[x].fa=merge(t[x].l,t[x].r);//更新x的孩子的父节点,把重新合并后的根赋值给x的父节点
}
int main()
{
	cin>>n>>m;
	t[0].dist=-1;//为什么初始化为-1?为了保证叶子节点的dist为0
	for(int i=1;i<=n;i++)cin>>t[i].val,t[i].fa=i;//输入顺便初始化
	for(int i=1;i<=m;i++){
		cin>>op>>x;
		if(op==1){
			cin>>y;
			if(t[x].val==-1||t[y].val==-1)continue;
			int X=Find(x),Y=Find(y);
			if(X!=Y)
				t[X].fa=t[Y].fa=merge(X,Y);
		}
		else {
			if(t[x].val==-1) {puts("-1");}//如果已经被删除了直接输出-1
			else {
				int X=Find(x);
				printf("%d\n",t[X].val);//输出堆顶元素
				pop(X);//删除堆顶元素
			}
		}
	}
	return 0;
}

罗马游戏

分析

题目说的花里胡哨的 其实还是个板子

简化题意:命令为\(M\)时把\(i\),\(j\)合并。

命令为\(K\)时删除分数最低元素,故为一个小根堆。

注意,若已经被删除了,要输出\(0\),而不是\(-1\)

代码实现

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m,x,y;
struct node{
	int l,r,fa,val,dist;
}t[N];
char op;
int merge(int x,int y){
	if(!x||!y) return x+y;
	if(t[x].val>t[y].val||t[x].val==t[y].val&&x>y)swap(x,y);
	t[x].r=merge(t[x].r,y);
	t[t[x].r].fa=x;
	if(t[t[x].l].dist<t[t[x].r].dist)swap(t[x].l,t[x].r);
	t[x].dist=t[t[x].r].dist+1;
	return x;
}
int Find(int x){
	if(t[x].fa==x) return x;
	t[x].fa=Find(t[x].fa);
	return t[x].fa;
}
void pop(int x){
	t[x].val=-1;
	t[t[x].l].fa=t[x].l;
	t[t[x].r].fa=t[x].r;
	t[x].fa=merge(t[x].l,t[x].r);
}
int main()
{
	scanf("%d",&n);
	t[0].dist=-1;
	for(int i=1;i<=n;i++)
		scanf("%d",&t[i].val),t[i].fa=i;
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		cin>>op;scanf("%d",&x);
		if(op=='M'){
			scanf("%d",&y);
			if(t[x].val==-1||t[y].val==-1) continue;
			int X=Find(x),Y=Find(y);
			if(X!=Y)
				t[X].fa=t[Y].fa=merge(X,Y);
		}
		else {
			if(t[x].val==-1){puts("0");}
			else {
				int X=Find(x);
				printf("%d\n",t[X].val);
				pop(X);
			}
		}
	}
	return 0;
}

其实可以发现,和第一个例题不一样的地方只有输入那里。这其实是我直接复制的模板然后改了改。

Monkey King

分析

是不是很熟悉? 是的没错,这就是夏令营\(day1\)的例题。

简化题意:使\(x\)\(y\)\(val\)减半后重新合并,并输出合并后堆的根。

若两个猴子在同一个堆中,输出\(-1\)

另一个与前两个题不同的地方是,这个题是个大根堆。

因为每次都是强壮值最大的进行战斗。

所以在合并时,只需要把

if(t[x].val>t[y].val)swap(x,y);

改为

if(t[x].val<t[y].val) swap(x,y);

即可。

注意,这个题是多测,记得清空数组。

是不是很简单!

代码实现

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,x,y;
struct node{
	int l,r,fa,val,dist;
}t[N];
int Find(int x){
	if(t[x].fa==x) return x;
	t[x].fa=Find(t[x].fa);
	return t[x].fa;
}
int merge(int x,int y){
	if(!x||!y) return x+y;
	if(t[x].val<t[y].val) swap(x,y);
	t[x].r=merge(t[x].r,y);
	t[t[x].r].fa=x;
	if(t[t[x].l].dist<t[t[x].r].dist) swap(t[x].l,t[x].r);
	t[x].dist=t[t[x].r].dist+1;
	return x;
}
int pop(int x){
	t[x].val>>=1;
	int new_root=merge(t[x].l,t[x].r);
	t[x].l=t[x].r=0;t[x].dist=-1;
	return t[x].fa=t[new_root].fa=merge(new_root,x);
}
int main()
{
	while(cin>>n){
		memset(t,0,sizeof(t));
		t[0].dist=-1;
		for(int i=1;i<=n;i++){
			scanf("%d",&t[i].val);
			t[i].fa=i;
		}
		scanf("%d",&m);
		for(int i=1;i<=m;i++){
			scanf("%d%d",&x,&y);
			int X=Find(x),Y=Find(y);
			if(X==Y){
				puts("-1");
			}
			else {
				int l=pop(X),r=pop(Y);
				t[l].fa=t[r].fa=merge(l,r);
				printf("%d\n",t[t[l].fa].val);
			}
		}
	}
	return 0;
}

左偏树当然不止这些 剩下的操作大家完全可以自学了((

posted @ 2023-06-21 08:37  XYini  阅读(55)  评论(0编辑  收藏  举报