文章目录
- 解析
- 可以解决的问题
- 定义:
- 左偏树的基本性质
- 基本结论
- 操作
- 合并
- 访问与删除堆顶元素
- 插入元素
- 批量插入
- 删除已知元素
所谓左偏树,就是往左偏的树
下面介绍一下它的一个兄弟:
《右偏树》
(逃)
解析
所谓左偏树,确实就是往左偏的树
它舍弃了平衡的性质,使这个堆在合并时变得极为高效
所以又叫做可并堆
代码实现起来还是很好写的
可以解决的问题
似乎基本上是起一个工具人的作用
出现在题解中“…对于某某信息,维护可并堆即可”这样的地方
不会确实不行呀 qwq
定义:
外结点 :左儿子或右儿子是空结点的结点。
距离 : 一个结点 x 的距离 dist(x)定义为其子树中与结点 x 最近的外结点到 x 的距离。特别地,定义空结点的距离为 -1 。
左偏树的基本性质
左偏树具有堆性质 ,即若其满足小根堆的性质,则对于每个结点 x 有vx<vlsv_x<v_{ls}vx<vls且vx<vrsv_x<v_{rs}vx<vrs
左偏树具有 左偏性质 ,即对于每个结点 x ,有 distlc≥distrcdist_{lc}\ge dist_{rc}distlc≥distrc
基本结论
结点 x 的距离 distx=distrc+1dist_x=dist_{rc}+1distx=distrc+1
距离为 n 的左偏树至少有 2n+1−12^{n+1}-12n+1−1个结点。此时该左偏树的形态是一棵满二叉树。
有 n 的结点的左偏树的根节点的距离是 O(log2n)O(\log_2 n)O(log2n)
(以上参考自:https://www.luogu.com.cn/blog/hsfzLZH1/solution-p3377)
上面的性质还是比较显然的
下面我们来讲讲具体的操作
操作
合并
左偏树的灵魂
设当前是小根堆,要把x堆与y堆合并
首先,如果x、y有一个为空,直接返回另一个
否则,把值较小的作为根,把根的右儿子与另一个堆合并
递归即可
递归回来的时候还要更新dist并维护左偏的性质
由于左偏树极右链是不超过logn的
因此复杂度为logn
写起来也很好写
int merge(int x,int y){if(!x) return y;else if(!y) return x;if(val[x]>val[y]||(val[x]==val[y]&&id[x]>id[y])) swap(x,y);rs[x]=merge(rs[x],y);if(dis[ls[x]]<dis[rs[x]]) swap(ls[x],rs[x]);dis[x]=dis[rs[x]]+1;return x;
}
访问与删除堆顶元素
把堆顶的左右儿子合并并返回即可
int del(int &x){int res=val[x];x=merge(ls[x],rs[x]);return res;
}
插入元素
把一个元素当成一棵树,与大树合并即可
void insert(int &r,int v){int x=New(v);r=merge(r,x);
}
批量插入
直接插入n次nlogn
开一个队列,把所有元素建成单点的树push进去
每次从队首提两棵树合并并放到队尾
直至只剩下一棵树
复杂度O(n)
void build(){queue<int>q;for(int i=1;i<=n;i++){q.push(New(a[i]));}for(int i=1;i<n;i++){int u=q.front();q.pop();int v=q.front();q.pop();q.push(merge(u,v));}r=q.front();q.pop();
}
删除已知元素
这是一个左偏树的重要优势
它可以在logn的时间内删除任意位置的已知元素
注意这里的“已知”是指已知编号而不是已知权值
删除指定权值的操作是堆很难做到的 (至少对我的知识储备来说)
注意树的性质可能变,所以要一直往上更新
void Del(int x){int f=fa[x];int p=merge(ls[x],rs[x]);fa[p]=f;if(!f){r=p;return;}if(f&&ls[f]==x) ls[f]=p;if(f&&rs[f]==x) rs[f]=p;while(f){if(dis[ls[f]]<dis[rs[f]]) swap(ls[f],rs[f]);if(dis[f]==dis[rs[f]]+1) return;dis[f]=dis[rs[f]]+1;p=f;f=fa[f];}return;
}
thanks for reading!