左偏树(可并堆)\((Leftist Tree)\)
前置知识:
- 堆及其简单运用
前言:
- 对于很多需要堆的操作,我们都可以用\(STL\)中的\(priority\_queue\)解决。
- 比如\(heap\_dijkstra\),对顶堆动态维护中位数,\(Huffman\)树等....
- 但是当我们需要支持一些别的操作的时候,可能就需要我们抛弃\(STL\),手搓这个数据结构。比如当我们遇到需要支持删除操作时,我们需要用手写堆。
- 当要求支持两个堆合并的时候,这时候我们就需要用到左偏树这个东西啦!
初识左偏树:
- 顾名思义,左偏树就是一棵向左偏的树。
- 左偏树又叫可并堆,是一种可以合并的堆,合并操作的复杂度为\(log(n_1+n_2)\),十分的优秀。
- 接下来看一张表来对比一下普通堆和左偏树(可并堆)
堆 | \(O(logn)\) | \(O(1)\) | \(O(logn)\) | \(O((n_1+n_2)log(n_1+n_2))\) |
左偏树 | \(O(logn)\) | \(O(1)\) | \(O(logn)\) | \(O(logn_1+logn_2)\) |
- 可以结合此图加深理解:
定义:
- 左偏树是一棵二叉树,他的每个节点存储\(4\)个值,分别为左右子树,权值,距离。
- 权值就是堆中的值。
- 定义一个节点为外节点,当且仅当这个节点的左子树和右子树中的一个是空节点。(外节点不是叶子节点。)
- 一个点的距离,定义为离它最近的外节点到这个节点的距离。
- 可以结合此图加深理解
(图源: 百度百科)
- 其中蓝色的数字为距离。特殊的: 规定空节点的距离为\(-1\)
性质:(基于小根堆)
- \(1:\) (堆性质) 左偏树中,父节点的权值小于等于他的孩子的权值。
- 解释: 左偏树又叫可并堆,好歹满足一下堆性质嘛。
- \(2:\) (左偏性质):左偏树中任意一个节点的左儿子距离一定大于等于右儿子距离。
- 解释: 不然怎么左偏嘛。
- 需要注意的一点,左偏指的不是左右儿子的大小。因此即使左儿子是一个点,右儿子是一个很长的链,那么也是满足要求的左偏树。
- 同时注意,距离和深度不是一个概念,左偏树并不意味着左子树的节点数或者深度一定大于右子树。
- \(3:\) 对于左偏树的任意节点,满足他的左右子树(如果有的话)均为左偏树。
- \(4:\) 左偏树任意一个节点的距离为其右儿子的距离\(+1\)
- 解释: 直观理解,很显然
- \(5:\) \(n\)个点的左偏树,距离最大为\(log(n+1)-1\)。(这个推论成为了左偏树时间复杂度的保证)
- 证明: 我不会证\(QwQ\)。
- 如果有会证的奆奆欢迎评论区或私信告诉我。
- 证明: 我不会证\(QwQ\)。
操作:(基于小根堆)
- 合并:(可并堆最基础操作,插入和删除两种操作都离不开他)
- 合并的原理是把一颗子树
- 当前有两个堆\(A,B\),假设\(A\)的根节点小于\(B\)的根节点(不满足也很简单,交换\(A,B\)即可),把\(A\)的根节点作为新的树\(C\)的根节点。
- 之后用\(A\)的右子树和\(B\)合并。
- 比较左右子树大小,如果右子树偏大的话,交换左右子树
- 最后运用性质\(4\)计算一下根节点的距离
- 插入:
- 插入就相当于只有一个节点的堆与另一个堆合并
- 删除:
- 删除这个节点相当于将这个堆拆分成了两棵树
- 然后将这两棵树再合并就行了
代码:
模板题
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
struct LeftistTree
{
int l, r; //左右子树
int fa; //父亲节点
int val; //点权
int dist; //距离
#define fa(x) ltree[x].fa
#define dist(x) ltree[x].dist
#define val(x) ltree[x].val
#define l(x) ltree[x].l
#define r(x) ltree[x].r
}ltree[maxn];
inline int get_fa(int x)
{
if(fa(x) == x) return x;
return fa(x) = get_fa(fa(x));
}
//只需要把根节点丢上来就好了
int merge_tree(int x, int y)
{
//如果有一颗空树 直接合并
if(!x || !y) return x + y;
if(val(x) > val(y) || (val(x) == val(y) && x > y))
swap(x, y);
r(x) = merge_tree(r(x), y);
fa(r(x)) = x;
if(dist(l(x)) < dist(r(x)))
swap(l(x), r(x));
dist(x) = dist(r(x)) + 1;
return x;
}
void pop_node(int x)
{
int l = l(x), r = r(x);
val(x) = -1; //把这个点删了
fa(l) = l; fa(r) = r; //把原先的堆拆成两棵子树
fa(x) = merge_tree(l, r); //将两棵树合并
}
int n, m;
int main()
{
//小根堆的个数和操作的个数
scanf("%d%d", &n, &m);
//输入每一个堆的初试一个节点
for(int i = 1, x; i <= n; i++)
{
scanf("%d", &x);
fa(i) = i; val(i) = x;
}
for(int i = 1, op, x, y; i <= m; i++)
{
scanf("%d%d", &op, &x);
if(op == 1) //将堆x和堆y合并
{
scanf("%d", &y);
if(val(x) == -1 || val(y) == -1)
continue;
x = get_fa(x); y = get_fa(y);
if(x != y) merge_tree(x, y);
}
else
{ //输出第x个堆的堆顶 并删除堆顶
if(val(x) == -1) puts("-1");
else
{
x = get_fa(x);
printf("%d\n", val(x));
pop_node(x);
}
}
}
return 0;
}