左偏树初探 By cellur925
$Intro$
假如有两个堆,我们想把它俩合并,怎么搞?
其实不用把两个堆中的元素一个一个抠出来啦x,我们有更厉害的数据结构--可并堆!
今天我们就来看看可并堆中最亲民的一种--左偏树!
$Properties$
- 堆的性质
- 既然他是一个(可并)堆,那它一定有堆的性质啦qwq。以小根堆为例,节点的权值小于等于它左右儿子的权值。
- 树的性质
- 准确地说它是一个二叉树qwq。又因为它的名字:左偏,所以还有一些特殊性质。
- 我们先来定义一个量:距离。这里的距离表示此节点到它子树中最近叶子节点的距离。叶子节点的距离为0。
- 而左偏树满足这样的性质:节点的左儿子的距离大于等于右儿子的距离。
比如这张图图自@CrazyAC,侵删
可以把它感性理解为一个半身不遂向左偏的病人(逃)
$Operation$
在介绍具体操作之前,我们先来看看为了维护上述性质我们需要保存记录一些什么量。
左右儿子的编号($lch$,$rch$)。节点的权值($val$)。节点的距离(意义见上)($dis$)。
另外还需要$fa$来存储父节点编号(直接父节点,注意与并查集区分)(至于为什么需要它,我们接下来再说)
下面我们来seesee几个基本操作:合并、插入、删除堆顶。
- 合并
- 由于我们在合并的操作给定的输入大多都是“合并$x$,$y$点所在的堆”,那么我们就需要$fa$来记录$x$,$y$的位置关系,但是这里注意,我认为它并不是并查集。因为并查集通常都有路径压缩,在压完(随时更新)后还要把$fa$值更新为$getf(x)$。
int getf(int x)//并查集 { if(x==fa[x]) return x; else return fa[x]=getf(fa[x]);//注意这里! }
也就是说,并查集中的$fa$是随时需要更新成总父亲的。而我们左偏树中的$fa$永远是直接(临时)父亲,不会改变,$getf$操作,只是用$x$自己去找到自己的总父亲(暴力向上跳)
左偏树的$getf$操作:
int getf(int x) { while(fa[x]) x=fa[x]; return x; }
-
- 所以说,我们合并的,其实是两个待查询节点的总父亲代表的两个待并堆。
主程序中的合并
scanf("%d%d",&x,&y); if(val[x]==-1||val[y]==-1) continue;//如果已经删除过,不予操作 int xx=getf(x); int yy=getf(y); if(xx==yy) continue; merge(xx,yy);
-
- 有了上述思想,剩下的我们就只是维护我们之前所说的性质了。
合并函数
int merge(int x,int y) {//返回的是合并两个堆后的堆顶 if(x==0||y==0) return x+y; if(val[x]>val[y]||(val[x]==val[y]&&x>y)) swap(x,y);//维护小根堆性质 //这里想让x当堆顶 (根) rch[x]=merge(rch[x],y);//将y所在堆与x的右儿子部分的堆合并 fa[rch[x]]=x;//记录右儿子的父亲 即x if(dis[lch[x]]<dis[rch[x]]) swap(lch[x],rch[x]);//维护左偏性质 dis[x]=dis[rch[x]]+1;//更新距离性质 return x; }
- 插入
- 插入一个点,就是把一个点和一个树合并起来。
- 删除一个数
- 合并它的左右儿子
void pop(int x) { val[x]=-1; fa[lch[x]]=fa[rch[x]]=0; merge(lch[x],rch[x]); }
待填坑
独立意志与自由思想是必须争的,且须以生死力争。