欢迎转载,另见版权声明↑↑。
左偏树是一种可并堆(顾名思义)。
l
e
f
t
left
left和
r
i
g
h
t
right
right分别表示左右儿子。
我们需要对每个节点维护一个
d
i
s
t
dist
dist,表示该节点往下走到空节点的距离。
“左偏”意为
d
i
s
t
[
l
e
f
t
]
≥
d
i
s
t
[
r
i
g
h
t
]
dist[left]\geq dist[right]
dist[left]≥dist[right]。
算法流程
左偏树最基础也是其突出特点就是合并两个堆,流程如下:
假设两个堆的根分别为
x
x
x和
y
y
y,不妨令
x
≤
y
x\leq y
x≤y(这里我们考虑小根堆)。
以
x
x
x为根,将
x
x
x的右子树和
y
y
y递归合并得到的右子树作为
x
x
x的新右子树。
然后维护左偏性质,如果
d
i
s
t
[
l
e
f
t
]
<
d
i
s
t
[
r
i
g
h
t
]
dist[left]<dist[right]
dist[left]<dist[right]将左右儿子交换。
两个堆合并流程为:
最后得到
左偏树其他操作均是借助合并完成的:
pop 删除根然后将两个子树合并
push 新建一个为该值的节点,然后将该节点看成一个堆,和原堆合并。
同时,其操作和 FHQ Treap 有点类似,我们还可以进行标记下传,但是只能整堆标记,从而达到批量增/减的目的。
时间复杂度
先证明,一棵有
n
n
n个节点的树,
d
i
s
t
≤
l
o
g
2
n
dist\leq log_2n
dist≤log2n。
那么,如果我们从根节点出发,每次都往小的子树走。因为每次都是往小的子树走,这棵子树的规模至多是原来的一半,那么走到空节点的路程
n
u
m
≤
l
o
g
2
n
num\leq log_2n
num≤log2n,所以
d
i
s
t
≤
l
o
g
2
n
dist\leq log_2n
dist≤log2n。
而依照上述流程,将两棵大小分别为
n
n
n和
m
m
m的堆
x
x
x和
y
y
y合并,所要递归的次数为
d
i
s
t
[
x
]
+
d
i
s
t
[
y
]
dist[x]+dist[y]
dist[x]+dist[y],所以时间复杂度为
O
(
l
o
g
2
n
m
)
O(log_2nm)
O(log2nm)。
例题
#include<bits/stdc++.h>
#define N 100002
using namespace std;
bool del[N];
int father[N],l[N],r[N],num[N],dist[N],root[N];
int merge(int x,int y){
if(x&&y){
if(num[x]>num[y]||(num[x]==num[y]&&x>y))swap(x,y);
father[r[x]=merge(r[x],y)]=x;
if(dist[l[x]]<dist[r[x]])
swap(l[x],r[x]);
dist[x]=dist[r[x]]+1;
return x;
}
return x|y;
}
int find(int x){return x==root[x]?x:(root[x]=find(root[x]));}
int main(){
int n,m,x,y;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",num+i),
dist[i]=1,root[i]=i;
while(m--){
scanf("%d%d",&n,&x);
if(n==1){
scanf("%d",&y);
if(del[x]||del[y]||(x=find(x))==(y=find(y)))continue;
root[x]=root[y]=merge(x,y);
}
else{
if(del[x])printf("-1");
else{
del[x=find(x)]=true,printf("%d",num[x]);
root[l[x]]=root[r[x]]=root[x]=merge(l[x],r[x]);
}
putchar('\n');
}
}
return 0;
}