p3377 左偏树模板 (左偏树)

上次学数据结构的时候还是10天前,那天学了莫队(暴力而优雅),现在又开始学数据结构了呢。

今天搞的是左偏树。

左偏树

一,左偏树是什么?

相信大家都知道什么叫做堆吧。

根据我们对堆的了解,我们知道,普通的堆是不支持合并操作的。

但是万恶的CCF不知道会在考场上给你整出什么古怪的要求。

于是,我们急切地需要一种可以合并的堆(自然你手写平衡树也是可以的)

那么,作为一种精巧美妙而又比较好写的数据结构,左偏树也就应运而生;

左偏树是一种支持在O(\(log_2n\))时间复杂度内进行合并的数据结构

自然插入操作也是O(\(log_2n\))的。

并且,通过一些神奇的操作,我们可以在同样O(\(log_2n\))的复杂度内找到一个点所在左偏树的根节点。

二,左偏树的定义与基本性质。

定义:

1.空节点:如果一个节点有儿子是空的,那么我们称这个节点为空节点。

2.\(dist\):节点x的距离\(dist[x]\)代表从这个结点出发,只经过右儿子到达一个空节点最少需要的边数。

性质:

1.堆性质:既然我们写的左偏树是一种堆型数据结构,那自然是要满足堆性质的,即一个节点的优先级大于等于他的左右两个儿子(如果是一个小根堆,那么存在这个节点的权值小于等于他的左右两个儿子)

2.左偏性质:左偏树顾名思义,就是向左偏的树;

其实,对应到代码中,就是说 对于每个节点x,都有\(dist[ls]\)>=\(dist[rs]\)

对应到上面的介绍,我们显然可以得出:

1.对于节点x,他的距离\(dist[x]\)=\(dist[rs]+1\)
(因为我们的dist一定是走右儿子得到的,所以就是右儿子的\(dist+1\)

2.距离为n的左偏树至多有\(2^{(n+1)}-1\)个节点。这时这个左偏树的形态是一个满二叉树。并且这个左偏树的根节点的距离就是O(\(log_2n\))。

三,基本操作与实现技巧

左偏树是为了合并而生的,所以他最基本的操作就是合并操作了。

在下面的代码中,我们通过merge()函数实现合并操作。

因为左偏树左儿子的距离是大于右儿子的距离的,所以我们每次只需要将一个节点与另一个节点的右儿子合并就可以了,复杂度是O(\(log_2n\))的。

那么到底是谁与谁的右儿子合并呢?

考虑到我们左偏树是满足堆的性质的,所以在合并后也要满足堆的性质(本题我们写的是小根堆),那么就是说一个节点的权值一定要小于等于他的儿子,那么我们考虑对两个左偏树的根节点进行比较,取其中较小的作为根节点,用它的右子树和另一个左偏树进行合并。

如何进行比较?

我们在开结构体时可以直接重载<运算符,这样就可以直接比较了

struct node{
	int id,v;//每个节点有一个id,有一个权值v
	bool operator < (const node &x)const{//重载<运算符,如果两个权值相等,我们把id小的放在前面,不然我们把权值小的放在前面
		return v==x.v?id<x.id:v<x.v;
	}
}v[maxn];

显然,直接运用上述合并方式以后,可能就失去了左偏的性质,于是在每次合并完之后,我们判断对于节点x,是否\(dist[ls]\)>=\(dist[rs}\),如果不是,那么我们需要将两个子树进行交换。并且更新\(dist[x]\)=\(dist[rs]+1\)

int merge(int x,int y){//对x,y两棵左偏树树进行合并操作
	if(!x||!y)return x+y;//如果其中有一个左偏树是空的,那么我们直接返回另一个左偏树的根节点(具体意思就是反正有一个是空的,那么我加上0以后还是它本身

	if(v[y]<v[x])swap(x,y);//不妨设x的根节点<=y的根节点,如果不是,我们交换一下
	rs[x]=merge(rs[x],y);//将x的右子树与y合并
	if(dist[ls[x]]<dist[rs[x]])swap(ls[x],rs[x]);//如果不满足左偏性质了,我们交换两个子树
	dist[x]=dist[rs[x]]+1;//更新x的dist
	return x;//返回新的左偏树的根节点
}

显然只完成这些我们是通不过p3377的

还需要查询,删除操作。

根据堆的性质,我们得知最小的值就是堆顶,即根节点。

若要删除,我们删除根节点,然后合并左右两个子树即可,同时要维护一些信息。

那么如何给定一个节点,删除其所在左偏树的根节点呢?(题目中第二问)

我可以记录下每个节点的父亲节点,然后一层层找,就可以找到根节点了;

这个过程,貌似和并查集有点像?

没错就是并查集!

那么问题来了,我们的左偏树虽然距离是log级别,但是退化成一条链的左偏树的深度甚至可能是O(n)的,那么就和并查集处理的方式一样了。

考虑路径压缩;

int find(int x){
	return rt[x]==x?x:rt[x]=find(rt[x]);
}

于是我们就大大的简化了复杂度。( O(n)->O(\(log_2n\)) )

我们在合并时,令\(rt[x]\)=\(rt[y]\)=\(merge(x,y)\);

在删除左偏树中最小值时,我们需要令\(rt[ls[x]]\)=\(rt[rs[x]]\)=\(rt[x]\)=\(merge(ls[x],rs[x])\);

原因:因为 \(x\) 是之前左偏树的根节点,在路径压缩时可能有 \(rt\) 的值等于 \(x\) ,所以$ rt[x] $也要指向删除后的根节点。

于是,这个题我们就可以AC了;

CODE:

#include<bits/stdc++.h>
using namespace std;
const int maxn=100010;

int n,m,x,y,cas;
int ls[maxn],rs[maxn],dist[maxn],rt[maxn];

bool vis[maxn];//用来记录这个节点是否已经被删除

struct node{
	int id,v;
	bool operator < (const node &x)const{
		return v==x.v?id<x.id:v<x.v;
	}
}v[maxn];

int find(int x){
	return rt[x]==x?x:rt[x]=find(rt[x]);
}

int merge(int x,int y){
	if(!x||!y)return x+y;
	if(v[y]<v[x])swap(x,y);
	rs[x]=merge(rs[x],y);
	if(dist[ls[x]]<dist[rs[x]])swap(ls[x],rs[x]);
	dist[x]=dist[rs[x]]+1;
	return x;
}

int main(){
	dist[0]=-1;
	
	scanf("%d%d",&n,&m);
	
	for(int i=1;i<=n;i++){
		scanf("%d",&v[i].v),rt[i]=i,v[i].id=i;
	}
	while(m--){
		scanf("%d",&cas);
		
		if(cas==1){
			scanf("%d%d",&x,&y);
			if(vis[x]||vis[y])continue;//如果其中一个节点被删除了,我们跳过他
			x=find(x),y=find(y);
			if(x!=y)rt[x]=rt[y]=merge(x,y);//如果他俩还没合并,我们就合并
		}
		
		if(cas==2){
			scanf("%d",&x); 
			if(vis[x]){//如果已经被删除了,输出-1
				printf("-1\n");
				continue;
			}
			x=find(x);
			printf("%d\n",v[x].v);
			vis[x]=true;//标记被删除了
			rt[ls[x]]=rt[rs[x]]=rt[x]=merge(ls[x],rs[x]);
			ls[x]=rs[x]=dist[x]=0;
		}
	}
	
	return 0;
}
posted @ 2021-04-23 15:49  LawrenceSivan  阅读(60)  评论(0编辑  收藏  举报