[2018.12.6]左偏树
其实NOIp之前就学会了...结果咕到了现在...
我们都知道堆。但是很少有人会手写堆。因为我们有STL,而且手写堆码量不小(据说是吧?没写过)。
而且堆的\(Merge\)操作又慢又麻烦。
于是就有了可并堆。
即使c++也有自带的可并堆
左偏树就是其中之一。
什么是左偏树
就是一颗向左偏的树。(逃
看一个例子:
(来源:百度图片)
好像确实左偏...
左偏树中,定义一个外节点为:左孩子或者右孩子为空的节点。
而一个节点的距离为它的子树中与它最近的外节点的距离。
外节点的距离为0,空节点的距离为-1(方便计算)。
距离在图中用蓝色字体标出。
接下来的内容中,我们维护大根堆(小根堆类似),对于左偏树的一个节点,其定义如下:
struct Ltree{//丑陋的结构体名
int v,d,f,c[2];
//v:节点的值 d:距离 f:父亲 c[0]:左孩子 c[1]:右孩子
}t[Size];//丑陋的变量名
左偏树的性质
左偏性质(不然干嘛叫左偏树):
对于任意节点\(x\),有\(t_{t_x.c_0}.d\ge t_{t_x.c_1}.d\),说人话就是一个节点的左孩子的距离大于它右孩子的距离。
这个性质保证左偏树的时间复杂度(介绍合并操作的时候会让大家感性理解这一点)。
堆性质(不然干嘛叫可并堆):
对于任意节点\(x\),有\(t_x.v\ge t_{t_x.c_0}.v\),\(t_x.v\ge t_{t_x.c_1}.v\),说人话就是任何一个节点的值大于等于它所有孩子的值。
这个性质保证左偏树的正确性。
左偏树的操作
注意:如果像模板题中一样需要判断是否被删除,需要另行记录。
找根操作(\(Find(x)\))
找到\(x\)节点的根。
不停跳\(t_x.f\)即可。
code:
int Find(int x){
while(t[x].f)x=t[x].f;
return x;
}
合并操作(\(Merge(x,y)\))
将以\(x,y\)为根的堆并在一起。
由于要维护堆性质,合并它们时,要让值大的在上面。
我们假设\(x\)为根,那么如果\(t_x.v<t_y.v\),就交换\(x\)和\(y\)。
然后呢?不用管然后了,直接递归到\(Merge(t_x.c_1,y)\)即可。
注意这里我们把\(y\)和\(t_x.c_1\)合并,而不是\(t_x.c_0\),就是因为左偏性质,右边的距离更小,在右边进行合并可以维持它的平衡,确保复杂度。(你要我严谨证明我也不会啊...)
然后此时\(t_x.c_0\)的距离有可能小于\(t_x.c_1\),如果出现这种情况,交换\(x\)的左右儿子。
然后需要更新\(x\)的距离。
显然\(t_x.d=t_{t_x.c_1}.d+1\)。应该很容易理解。
code:
void Merge(int &x,int &y){
if(!y)return;//结束
if(!x){//结束
swap(x,y);
return;
}
if(t[x].v<t[y].v)swap(x,y);//维护堆性质,交换x,y
Merge(t[x].c[1],y);
t[t[x].c[1]].f=x;//更新右孩子的父亲
if(t[t[x].c[0]].d<t[t[x].c[1]].d)swap(t[x].c[0],t[x].c[1]);//维护左偏性质,交换x的左右孩子
t[x].d=t[t[x].c[1]].d+1;//更新距离
}
删除操作(\(Delete(x)\))
删除节点\(x\)所在堆的最大值。
找到\(x\)所在左偏树的根,将根的左右孩子的父亲设为空,合并它们即可。
code:
void Delete(int x){
t[t[x].c[0]].f=t[t[x].c[1]].f=0;
Merge(t[x].c[0],t[x].c[1]);
}
Upd(2018.12.12)
才知道删除还可以删除任意已知节点。。。
这里的已知指我们可以直接访问它的位置,而不是知道它的值。比如左偏树不能完成例如删除树中值为233的节点,但是可以进行例如删除编号为233的节点(我们可以直接访问233号节点)。
类似删除根的方法,我们先合并它的左右子树,然后一直跳它的父亲:
当他父亲的距离等于新合并的子树的距离+1,就可以停止了;
当它的父亲的距离大于新合并的子树的距离+1,更新父亲的距离,如果这棵子树是父亲的左子树的话还需要交换父亲的子树;
当它的父亲的距离小于新合并的子树的距离+1,如果这棵子树是父亲的左子树就停止,否则更新父亲的距离为两棵子树距离的较小值+1,如果左子树的距离更小就交换左子树。
因为作者很懒所以代码不给出。
最值操作(\(Max(x)\))
返回\(x\)所在左偏树的最大值。
也就是返回根的值啦!
code:
int Max(int x){
return t[Find(x)].v;
}
没了。
这个数据结构是如此的简单。
还有一点要说的:关于给一个序列建左偏树。
给每个元素建一棵一个节点的左偏树,顺序合并即可。时间复杂度\(O(nlogn)\)。
讲完了。
最后附上模板题代码:
#include<bits/stdc++.h>
using namespace std;
struct Pair{
int v,id;
bool operator >(Pair y){
if(v!=y.v)return v>y.v;
return id>y.id;
}
bool operator <(Pair y){
if(v!=y.v)return v<y.v;
return id<y.id;
}
};
struct node{
int f,d,c[2];
Pair v;
}t[100010];
int n,m,op,u,v,fu,fv,del[100010];
int getf(int x){
while(t[x].f)x=t[x].f;
return x;
}
void Merge(int &x,int &y){
if(!y)return;
if(!x){
swap(x,y);
return;
}
if(t[x].v>t[y].v){
swap(x,y);
}
Merge(t[x].c[1],y);
t[t[x].c[1]].f=x;
if(t[t[x].c[1]].d>t[t[x].c[0]].d){
swap(t[x].c[1],t[x].c[0]);
}
t[x].d=t[t[x].c[1]].d+1;
}
int Delete(int x){
t[t[x].c[0]].f=t[t[x].c[1]].f=0;
Merge(t[x].c[0],t[x].c[1]);
del[x]=1;
return t[x].v.v;
}
int main(){
scanf("%d%d",&n,&m);
t[0].d=-1;
for(int i=1;i<=n;i++){
scanf("%d",&t[i].v.v);
t[i].v.id=i;
t[i].d=0;
}
for(int i=1;i<=m;i++){
scanf("%d",&op);
if(op==1){
scanf("%d%d",&u,&v);
fu=getf(u);
fv=getf(v);
if(del[u]||del[v]||fu==fv)continue;
Merge(fu,fv);
}else{
scanf("%d",&u);
if(del[u]){
puts("-1");
}else{
fu=getf(u);
printf("%d\n",Delete(fu));
}
}
}
return 0;
}
练习题:
[APIO2012]派遣
->Luogu
->BZOJ
->题解