一、左偏树能做什么?
左偏树(Leftist Tree)是一种维护可并堆(Mergeable Heap)的数据结构。
可并堆是一种抽象数据结构(Abstract Data Type, ADT),在普通的堆(Heap)的基础上,增添了 upd 操作,使得两个堆可以合并。这也是其名之来历。
二、左偏树的基本操作
左偏树的基本操作有三种:
- 一切操作的核心:merge 操作(合并)
- del 操作【删除操作,分为删除堆顶元素(入门)和删除任意元素(进阶)】
- add 操作(加点操作)
本文将以洛谷的左偏树模板 P3377 为纲,故不会介绍 del 操作中的删除任意元素和 add 操作,同时,以下的堆实质上都为小根堆。
三、详解左偏树操作
-
Part ZERO: 有关左偏树的定义与基本性质、定理
左偏树是一颗二叉树(Binary Tree),它的节点除了和二叉树的节点一样具有左右子树(Left Subtree, ls; Right Subtree, rs)以外,还有两个属性:键值(Value, v)和距离(Distance, dis)。
-
Definition ONE: 外节点(External Node)
节点 (i) 被称为外节点,当且仅当节点 (i) 的左子树或右子树为空。
-
Definition TWO: 距离
节点 (i) 的距离是它到其后代中最近的外节点所经过的边数。
特别地,如果节点 (i) 本身就是外节点,则其距离为 (0); 空节点的距离被规定为 (-1).
通常称一棵左偏树的距离为其根节点的距离。
-
Property ONE: 任意节点的键值不大于它的左右子节点的键值(小根堆性质)。
由 Property ONE 可得:
-
Theorem ONE: 左偏树的根节点的键值是所有节点中最小的。
由 Theorem ONE, 我们可以用 (O(1)) 的时间复杂度得到左偏树中最小的节点。
-
Property TWO: 任意节点的左子节点的距离不小于其右子节点的距离(左偏性质)。
小根堆性质与左偏性质对于每一个节点都成立。因此,显然可以得出,左偏树的左右子树皆为左偏树。同时,我们可以得出左偏树之定义:具有左偏性质的堆有序二叉树是左偏树。
由 Property TWO 可知:
-
Property THREE: 任意节点的距离等于其右子节点的距离加 (1).
接下来,我们将要讨论左偏树的距离与节点数之间的关系。
-
Lemma ONE: 若一棵左偏树的距离为一定值,则在所有可能的左偏树当中,节点最少的是一棵完全二叉树。
Proof: 由 Property TWO 可证。
有 Lemma ONE 可知:
-
Theorem TWO: 若一棵左偏树的距离为 (k), 则这颗左偏树至少有 ((2^{k+1}-1)) 个节点。
由此推论可知:
-
Corollary ONE: 一棵 (n) 个节点的左偏树的距离最大值为 (log_2(n+1)-1).
-
-
Part ONE: merge 操作
merge 操作在代码中体现为
int merge(int x,int y)
函数。其中,令 (x) 为 (A) 堆的堆顶,(y) 为 (B) 堆的堆顶。merge 操作的任务即是将 (B) 堆与 (A) 堆的右子树合并,且维护左偏树的性质。merge 操作分为以下几个过程:
Step ONE: 特判。如果两个合并的堆有任意一个为空,则返回另一个堆。
Step TWO: 判断优先级。判断两堆堆顶的键值大小。如果 (x) 的键值大于 (y) 的键值,则说明如果仍然将当前的 (B) 堆与 (A) 堆的右子树合并,就无法维护左偏树的小根堆性质,故需要
swap(x,y)
, 即将 (A) 堆和 (B) 堆“交换”。Step THREE: 合并。将 (B) 堆与 (A) 堆的右子树合并。
Step FOUR: 判断距离。如果 (A) 的右子树的距离比左子树大,则违背了左偏树的左偏性质。则进行交换。
Step FIVE: 更新距离。将整棵左偏树的距离进行更新。
代码如下:
const int maxn=100005; struct Node{ int ls; int rs; int v; int dis; int id; }tr[maxn]; int merge(int x,int y){ if(!x||!y)return x+y;//Step ONE if(tr[x].v>tr[y].v) swap(x,y);//Step TWO tr[x].rs=merge(tr[x].rs,y);fa[tr[x].rs]=x;//Step THREE if(tr[tr[x].rs].dis>tr[tr[x].ls].dis) swap(tr[x].ls,tr[x].rs);//Step FOUR tr[x].dis=tr[x].rs?(tr[tr[x].rs].dis+1):0;//Step FIVE return x; }
Part TWO: del 操作
del 操作相对来说非常容易理解,它所要做的就是删除一个堆的堆顶,并重新整理其他的节点使其重新成为一个堆。
删除堆顶很容易,我们只需要将堆顶节点的信息全部清空即可。
重新整理呢?我们换个角度想想。其实,我们只需要将原堆顶节点的两个子树使用 merge 操作合并!
代码如下:
int del(int x){
int fx=tr[x].ls;int fy=tr[x].rs;
tr[x].ls=0;tr[x].rs=0;
tr[x].dis=0;
fa[fx]=fx;fa[fy]=fy;
int tmp=merge(fx,fy);
return fa[x]=tmp;
}
Part THREE: 全部代码
下面的代码是洛谷 P3377 【模板】左偏树(可并堆)的 AC 代码。由于题目的要求,可能部分代码与上面的解释会有所不同,但不影响理解。
#include<bits/stdc++.h>
using namespace std;
void rd(int &x){
x=0;int f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
x*=f;
}
const int maxn=100005;
struct Node{
int ls;
int rs;
int v;
int dis;
int id;
}tr[maxn];
int fa[maxn];
bool exist[maxn];
int n,m;
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
int merge(int x,int y){
if(!x||!y)return x+y;
if(tr[x].v>tr[y].v||(tr[x].v==tr[y].v&&tr[x].id>tr[y].id))
swap(x,y);
tr[x].rs=merge(tr[x].rs,y);fa[tr[x].rs]=x;
if(tr[tr[x].rs].dis>tr[tr[x].ls].dis)
swap(tr[x].ls,tr[x].rs);
tr[x].dis=tr[x].rs?(tr[tr[x].rs].dis+1):0;
return x;
}
int del(int x){
int fx=tr[x].ls;int fy=tr[x].rs;
tr[x].ls=0;tr[x].rs=0;
tr[x].dis=0;
fa[fx]=fx;fa[fy]=fy;
int tmp=merge(fx,fy);
return fa[x]=tmp;
}
int main(){
rd(n);rd(m);
fill(exist+1,exist+n+1,true);
for(int i=1;i<=n;i++){
rd(tr[i].v);
tr[i].id=fa[i]=i;
}
for(int i=1;i<=m;i++){
int op,x,y;
rd(op);rd(x);
if(op==1){
rd(y);
if(!exist[x]||!exist[y])continue;
int fx=find(x);int fy=find(y);
if(fx!=fy)merge(fx,fy);
}else if(op==2){
if(!exist[x]){cout<<-1<<endl;continue;}
x=find(x);
cout<<tr[x].v<<endl;
del(x);
exist[tr[x].id]=false;
}
}
return 0;
}