左偏树
左偏树是一种可并堆(一系列的堆),支持以下操作:
-
删除一个堆的最值。
-
查询一个堆的最值。
-
新建一个堆,只包含一个元素。
-
合并两个堆。这个复杂度是 \(O(\log)\) 的。
左偏树是一颗二叉树。定义 “外结点” 为儿子数量不等于 \(2\) 的结点,定义每个结点的 \(dist\) 为该结点到最近的外结点的距离。
左偏树满足对于每个节点,左儿子的 \(dist>\) 右儿子的 \(dist\)。
左偏树满足堆的性质,即一个结点是它子树内的最值。
对于 2 操作,输出一个左偏树的根即可。
对于 3 操作,显然很简单。
如果我们实现了 4,则 1 就只需要合并根的左右儿子,然后删除根即可。
如何实现 4 ?我们观察到一个性质。如果左偏树从根出发一直向右走,最多走 \(\log n\) 步,就会到达一个没有子结点的结点 \(x\)。
证明:假设还能继续往下走。注意到 \(x\) 已经是最右的结点了,所以为了使它的祖先们满足左 \(dist>\) 右 \(dist\),如果截取深度为 \(1\sim dth[x]\) 的这一段,必构成满二叉树。满二叉树结点个数是 \(2\) 的幂,所以 \(d[x]\le \log n\)。
根据这个结论,可以设计出 \(O(\log n)\) 的合并堆算法:
-
初始有两颗左偏树 \(h1,h2\),不妨 \(h1\) 的根优于 \(h2\)。
-
合并 \(h1\) 右节点的子树 和 \(h2\),将合并出来的左偏树作为 \(h1\) 的右儿子。
-
如果此时 \(h1\) 右儿子的 \(dist\) 大于左儿子,交换左右儿子。
使用并查集快速查询某个结点所在堆的根。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
int n, m;
// 值类型,默认构造函数应保存最不优先的值
struct Val {
int v;
Val() {
v = inf;
}
Val(int x) {
v = x;
}
} ide;
// 定义为大的优先
bool operator<(Val x, Val y) {
return x.v > y.v;
}
struct LefHeap {
int sz;
vector<Val> a; // 值
vector<bool> f; // 是否未删除
vector<int> l, r, d, p; // 左子、右子、深度、并查集父结点
// 查找x号元素所在的根
int fnd(int x) {
if (p[x] == x)
return x;
return p[x] = fnd(p[x]);
}
// 合并x, y号元素为根的堆,返回根编号
int unn(int x, int y) {
if (x == 0 || y == 0) // 一方为空,另一方有效
return x + y;
if (a[x] < a[y]) // 否则,x为根
swap(x, y);
p[y] = x, r[x] = unn(r[x], y);
if (d[l[x]] < d[r[x]])
swap(l[x], r[x]);
d[x] = d[r[x]] + 1; //更新dist
return x;
}
// 加入新元素,值为x
void push(Val x) {
sz++;
l.push_back(0), r.push_back(0), d.push_back(0);
p.push_back(sz), a.push_back(x), f.push_back(true);
}
// 查找x号元素所在堆的顶
Val top(int x) {
return a[fnd(x)];
}
// 删除x号元素所在堆的顶
void pop(int x) {
int rt = fnd(x), new_rt = unn(l[rt], r[rt]);
f[rt] = 0, p[rt] = p[new_rt] = new_rt; // 并查集换根
}
LefHeap() {
sz = 0;
p = l = r = d = vector<int>(1, 0);
a = vector<Val>(1, ide);
f = vector<bool>(1, false);
}
} h;
int main()
{
cin >> n >> m;
for (int i = 1, t; i <= n; i++) {
cin >> t;
h.push(t);
}
for (int i = 1, op, x, y; i <= m; i++) {
cin >> op;
if (op == 1) {
cin >> x >> y;
if (h.f[x] && h.f[y] && h.fnd(x) != h.fnd(y))
h.unn(h.fnd(x), h.fnd(y));
}
else {
cin >> x;
if (h.f[x]) {
cout << h.a[h.fnd(x)].v << endl;
h.pop(x);
}
else
cout << -1 << endl;
}
}
return 0;
}
注:为什么删除的时候还要让 p[rt] = new_rt
?
因为此时我们想要原本所有指向 rt
的都指向 new_rt
,直接让 rt -> new_rt
即可。
注2:只能写 rt -> new_rt
,不能写 l[rt],r[rt] -> new_rt
,因为路径压缩时有的已经直接指向 rt
了。