左偏树

定义:

左偏树是 一种具有左偏性质的堆有序二叉树 (这里要注意,堆有序二叉树和二叉堆并不是同一种东西,因此左偏树并不是二叉堆)。——可并堆之左偏树

每一个节点存储的信息包括左右子节点、关键值、父节点以及距离。

等等,节点的距离并不是节点子树的大小,也不是深度

节点的距离 \(dist\) 的定义和性质

对于一棵二叉树,我们定义 外节点 为左儿子或右儿子为空的节点,定义一个外节点的 \(dist\) 为 0,
一个不是外节点的节点 \(dist\) 为其到子树中最近的外节点的距离加一。空节点的 \(dist\) 为 -1。

一棵有 个节点的二叉树,根的 \(dist\) 不超过 \(「\log(n+1)」\),因为一棵根的 \(dist\)\(x\) 的二叉树至少有 \(x-1\) 层是满二叉树,那么就至少有 \(2^x-1\) 个节点。
注意这个性质是所有二叉树都具有的,并不是左偏树所特有的。
—— OI Wiki

需要注意的是,\(dist\) 不是深度,左偏树的深度没有保证,一条向左的链也是左偏树。

所以说,左偏树既是一颗二叉树,又具有「左偏」的性质:这个节点的 左儿子的 \(dist\) 大于 右儿子的\(dist\)

下图为一棵左偏树:


操作:

可合并堆的操作,那肯定就是‘合并’了,为了找到合并的两个点的根,要用到‘并查集’

for (i = 1; i <= n; ++i) f[i] = i;
int find(int x) {
    return x == f[x] ? x : f[x] = find(f[x]); 
}

合并merge

合并操作是可并堆最重要的操作,以小根堆为例,合并\(x\)\(y\)

首先,我们要满足小根堆的性质,取\(x\)\(y\)中较小的一个作为根;
然后将 较小的节点的右儿子 与较大的节点合并

if(val[x] > val[y]) Swap(x, y);
rc[x] = merge(rc[x], y);

至于为什么是右儿子 与 较大的节点合并,这棵树毕竟是 左偏 的,我们连到 右子树 上是为了保证它偏的不是太厉害,并且右子树毕竟‘浅’嘛。否则,小心成‘链’。

等我们合并完,回溯的过程中,我们还要判断它是否还保持「左偏」,并且update一下根的dist

if(dist[rc[x]] > dist[lc[x]]) Swap(lc[x], rc[x]);
dist[x] = dist[rc[x]] + 1;

最后,我们还需要update它们的父亲father

f[find(x)] = f[find(y)] = merge(find(x), find(y));

【图解】<--还是推荐去看一下

代码

【合并mergeのCode】

f[find(x)] = f[find(y)] = merge(find(x), find(y));
int merge(int x, int y){
    if(!x || !y) return x + y;
    if(val[x] > val[y]) Swap(x, y);
    rc[x] = merge(rc[x], y);
    if(dist[rc[x]] > dist[lc[x]]) Swap(lc[x], rc[x]);
    dist[x] = dist[rc[x]] + 1;
    return x;
}

删除Delete

  • 删除根节点

我们将 根节点 的左右儿子 合并起来,自然 根节点 就被删除掉了,
最后记得更新 father 的值

【删除根节点のCode】

 f[x] = f[t[x].ch[0]] = f[t[x].ch[1]] = merge(t[x].ch[0], t[x].ch[1]);
  • 删除任意节点

这时候我们将 节点 的左右儿子 合并起来,然后自底向上更新 \(dist\) 、不满足左偏性质时交换左右儿子,当 \(dist\) 无需更新时结束递归:

代码

【删除节点のCode】

int merge(int x, int y){
    if(!x || !y) return x + y;
    if(val[x] > val[y]) swap(x, y);
    rc[x] = merge(rc[x], y);
    if(dist[rc[x]] > dist[lc[x]]) swap(lc[x], rc[x]);
    pushup(x);
    return x;
}
void pushup(int x) {
  if (!x) return;
  if (dist[x] != dist[rc[x]] + 1) {
    dist[x] = dist[rc[x]] + 1;
    pushup(fa[x]);
  }
}

插入节点insert

单个节点也可以视为一个堆,合并即可。

代码

【合并の新Code】

左偏树还有一种无需交换左右儿子的写法:
将 较大的儿子视作左儿子, 较小的儿子视作右儿子:

int  &rs(int x) {
    return ch[x][dist[rc[x]] < dist[lc[x]]]; 
}

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

整个堆加上/减去一个值、乘上一个正数

代码
点击查看代码 buhui

例题

【P3377 左偏树(可并堆)】
【模板】左偏树(可并堆)
题目描述

如题,一开始有 \(n\) 个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:

  1. 1 x y:将第 \(x\) 个数和第 \(y\) 个数所在的小根堆合并(若第 \(x\) 或第 \(y\) 个数已经被删除或第 \(x\) 和第 \(y\) 个数在用一个堆内,则无视此操作)。
  2. 2 x:输出第 \(x\) 个数所在的堆最小数,并将这个最小数删除>(若有多个最小数,优先删除先输入的;若第 \(x\) 个数已经被删除,则输出 \(-1\) 并无视删除操作)。
输入格式

第一行包含两个正整数 \(n, m\),分别表示一开始小根堆的个数和接下来操作的个数。
第二行包含 \(n\) 个正整数,其中第 \(i\) 个正整数表示第 \(i\) 个小根堆初始时包含且仅包含的数。
接下来 \(m\) 行每行 \(2\) 个或 \(3\) 个正整数,表示一条操作,格式如下:
操作 \(1\)1 x y
操作 \(2\)2 x

输出格式

输出包含若干行整数,分别依次对应每一个操作 \(2\) 所得的结果。

【完整代码】
#include<iostream>
#define M 100005
using namespace std;

inline void swap(int &x,int &y){
	int temp=x;x=y;y=temp;
}

int ch[M][2],fa[M],vis[M];

struct heap{
	int val,dis;
}h[M<<1];
int marge(int x,int y){
	if(!x||!y)return x+y;
	if(h[x].val>h[y].val)swap(x,y);
	ch[x][1]=marge(ch[x][1],y);
	if(h[ch[x][1]].dis>h[ch[x][0]].dis)swap(ch[x][0],ch[x][1]);
	h[x].dis=h[ch[x][1]].dis+1;
		return x;
} 

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

int main(){
	int n,m,opt,x,y;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&h[i].val);
		fa[i]=i;
	}
	for(int i=1;i<=m;i++){
		scanf("%d",&opt);
		if(opt==1){
			scanf("%d%d",&x,&y);
			if(vis[x]||vis[y])continue;
			x=find(x);
			y=find(y);
			if(x!=y)fa[x]=fa[y]=marge(x,y);
		}else{	
			scanf("%d",&x);
			if(vis[x])puts("-1");
			else{
				x=find(x);
				printf("%d\n",h[x].val);
				vis[x]=true;
				fa[ch[x][1]]=fa[ch[x][0]]=fa[x]=marge(ch[x][0],ch[x][1]);
				ch[x][0]=ch[x][1]=h[x].dis=0;
			} 
		}
	}
	return 0;
}

P3377 【模板】左偏树(可并堆)
P1456 Monkey King
P2713 罗马游戏

posted @ 2022-08-09 19:13  Ciaxin  阅读(59)  评论(0编辑  收藏  举报