左偏树,是一种可以在 (Theta(log_2 n)) 复杂度内合并的一种数据结构。
它是以二叉树的形态来维护堆的性质,由此也延伸出很多别的操作。
接下来本文所述堆及其操作均默认为小根堆。
一些约定
-
(v_i) 节点 (i) 的权值。
-
(l_i,r_i) 节点 (i) 的左右儿子。
-
(d_i) 节点 (i) 到其最近外节点的距离。
-
(f_i) 节点 (i) 的父节点。
什么是外节点:一个左儿子或右儿子是空节点的节点。
左偏树的性质
堆性质
作为堆式数据结构显然有堆的性质,即对任意一个树中的点 (i),(v_i<v_{l_i},v_i<v_{r_i})。
但是任意一个节点左右儿子的大小并不确定,这也不在我们的维护范围之内。
左偏性质
左偏,就是对于任意一个树中的点 (i),(d_{l_i}>d_{r_i})。
另外的,对于任意一个树中的点 (i),(d_i=d_{r_i}+1)。
左偏树的操作
合并操作
左偏树最核心的操作,其余的操作也均以这个操作为基础。
假设我们要合并分别以 (a,b) 为根节点的两棵左偏树。
首先,若其中一棵树的根为空节点,则另一棵树的根成为新树的根。
若两个树根均存在,那么权值更小的必定成为新的树根。
这里我们假设 (v_a<v_b),即 (a) 为新树的根。为了避免分类讨论,当 (v_a>v_b) 时,我们可以将其交换。
此时我们再令 (b) 与 (a) 的右儿子进行合并,重新比较其大小。如此不断比较下去直到遇到空节点或叶节点为止。
这样递归返回每次合并的根节点的值,重置此树的相关节点。
另外,由于是对右儿子进行合并,所以可能合并后的树不再满足左偏性质。此时我们交换左右子树。
我们要时刻保证对于任意一个树中的节点 (i),(d_i=d_{r_i}+1),所以我们重新规定一下右儿子的距离即可。
实现也比较容易。
int merge(int x,int y){
if(!x||!y) return x+y;
if(v[y]<v[x]) swap(x,y);
rs=merge(rs,y);
if(dis[ls]<dis[rs])swap(ls,rs);
dis[x]=dis[rs]+1;
return x;
}
还有一种写法是随机合并,引入随机数来判断是否交换左右节点。
这样做可能会使树不满足左偏性质,但是省去了距离的相关计算,且复杂度不变。
取最值操作
由于左偏树的堆性质,它的最值肯定在堆顶。那么就直接不断跳父节点就行了。
但是我们发现暴跳太慢了。我们可以借助并查集对其路径压缩。
对于同一棵左偏树中的点用并查集连接到一起,并钦定其祖先节点为堆顶。
于是就可以在 (Theta(log_2 n)) 的时间内快速找出最值了。
删除操作
删除操作只针对堆顶节点来说。
删除节点需要重置其信息(左右儿子和距离)。
另外,删除此节点之后,其真正的左右儿子并没有被删除,但是现在他们都没有了父亲(?),所以我们需要合并这两个点。
fa[lson[x]]=fa[rson[x]]=fa[x]=merge(lson[x],rson[x]);
注意这个节点本身的父亲也要指向合并后的根节点。
因为路径压缩后删除此节点,此节点的父亲指向的是自己,若不更改可能会导致后面出各种问题。
插入节点
等同于将其与一棵大小为一的堆合并。
此时要保证插入的这个点的各个信息已经处理完善。
全树加/减/乘一个值
维护懒标记,从根节点开始不断向下穿即可。具体是在删除/合并访问儿子时下传。
由于只能单个传,所以速度较慢。
当然不止这三种操作,可以打标记且不改变相对大小的操作都可以。
例题
Monkey King
给定初始的 (n) 个大小为一的堆及其权值,再给定 (m) 次询问。
每次询问x y
,表示将 (x) 与 (y) 所在的堆的堆顶权值变为原来的一半后合并。
对于每次操作,输出新堆的堆顶权值。
(n,mle 100000)
一开始的思路是,每次新建两个权值分别为两个堆顶权值一半的点,先将这两个点与这两个堆合并,再删除堆顶,最后合并两个堆。
至于权值方面,由于我懒得写大根堆,于是就将权值的相反数插入堆中建小根堆。
注意奇数的整除是下取整,所以对于奇数 (a),要将 a>>=1
写成 a=-((-a)>>1)
。
计算下来若每次新建两个点,那么总共就是 (2 imes m) 个,加上原来的应该不超过 (300000) 个点。
但不知道为什么总是 MLE,至今没找到原因(
所以我换了写法,不再新建节点,而是先删除两个堆顶,再在每个堆中加入新点,只不过点的编号还是原来堆顶的。
最后合并两个堆就做完了。
代码和板子的差不多。
namespace LIT{
#define ls lson[x]
#define rs rson[x]
int fa[maxn],dis[maxn];
int lson[maxn],rson[maxn];
struct node{
int pos,val;
bool operator < (const node &b) const{
return val^b.val?val<b.val:pos<b.pos;
}
}v[maxn];
int findf(int x){
return fa[x]==x?x:fa[x]=findf(fa[x]);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(v[y]<v[x]) swap(x,y);
rs=merge(rs,y);
if(dis[ls]<dis[rs])swap(ls,rs);
dis[x]=dis[rs]+1;
return x;
}
}
using namespace LIT;
signed main(){
dis[0]=-1;
while(scanf("%d",&n)!=EOF){
for(int i=1;i<=n;i++){
lson[i]=rson[i]=dis[i]=0;
v[i].val=read()*-1;
v[i].pos=fa[i]=i;
}
m=read();cnt=n;
for(int i=1,x,y;i<=m;i++){
x=findf(read());y=findf(read());
if(x==y){printf("-1
");continue;}
v[x].val=-((-v[x].val)>>1);
int rt1=merge(lson[x],rson[x]);
lson[x]=rson[x]=dis[x]=0;
int now1=fa[rt1]=fa[x]=merge(rt1,x);
v[y].val=-((-v[y].val)>>1);
int rt2=merge(lson[y],rson[y]);
lson[y]=rson[y]=dis[y]=0;
int now2=fa[rt2]=fa[y]=merge(rt2,y);
fa[now1]=fa[now2]=merge(now1,now2);
printf("%d
",-v[fa[now1]].val);
}
}
return 0;
}