左偏树
左偏树
\(\mathcal O(\log)\) 时间内合并两个堆,\(\mathcal O(1)\) 求堆顶。本文讨论的是小根堆。
一些定义
外结点:左儿子 / 右儿子为空的结点。
距离:\(x\) 的距离 \(dist_x\) 定义为其子树中与结点 \(x\) 最近的外结点到 \(x\) 的距离。特别的,空结点的距离为 \(-1\)。
左偏树的性质
- 堆的性质,即 \(val_{lc} \geq val_x, val_{rc} \geq val_x\)。
- 左偏性质,即 \(dist_{lc} \geq dist_{rc}\)。
结论
- 由于性质 2,那么 \(dist_x =dist_{rc} + 1\)。
- \(n\) 个结点的左偏树的根节点的距离是 \(\mathcal O(\log n)\) 的。
- 距离为 \(n\) 的左偏树至少有 \(2^{n+1} - 1\) 个结点。
2.3. 性质是所有二叉树都有的。
操作
合并操作
定义 \(merge(x, y)\) 表示合并两颗根节点分别为 \(x, y\) 的左偏树,返回值为合并后的根节点。
首先不考虑左偏性质,考虑如何合并两个堆:
- 若 \(val_x \leq val_y\) 则 \(x\) 作为合并后的根,否则 \(y\) 作为合并后的根。下文默认 \(val_x \leq val_y\)。
- 将 \(y\) 与 \(x\) 的某一儿子合并,合并后的根节点作为 \(x\) 的对应儿子。
- 重复 1.2. 操作,直至 \(x,y\) 有一方为空结点。
上述方案复杂度为 \(\mathcal O(n)\),为使复杂度更加优秀,我们有两种方式:
- 每次随机选择 \(x\) 的左右儿子进行合并。
- 使用左偏树的性质。
这里主要讨论方式 2,考虑到左儿子距离大于等于右儿子,每次合并都选择 \(x\) 的右儿子与 \(y\) 进行合并,考虑到 \(dist\) 是 \(\mathcal O(\log n)\) 级别的,故合并两颗大小为 \(a\) 和 \(b\) 的左偏树的时间复杂度便为 \(\mathcal O(\log a + \log b)\)。
每次合并之后可能会导致左偏性质的缺失,故每次合并之后判断是否有 \(dist_{lc} \geq dist_{rc}\),若无则交换 \(lc, rc\),后再更新 \(dist_x =dist_{rc} + 1\)。
inline int merge(int x, int y)
{
if (! x || ! y) return x + y;
if (val[x] > val[y]) std::swap(x, y);
rc[x] = merge(rc[x], y);
if (dist[rc[x]] > dist[lc[x]])
std::swap(lc[x], rc[x]);
dist[x] = dist[rc[x]] + 1;
return x;
}
插入操作
将单个元素看作一个堆,合并即可。
删除最小值
合并左右子树即可。
删除任意结点
将该结点的左右儿子合并,然后从该结点的父亲开始自底向上更新 \(dist\)、不满足左偏性质时交换左右儿子,当 \(dist\) 无需更新时结束递归。
复杂度分析:
令该结点父亲为 \(fa_x\),首先更新 \(dist_{fa_x}\),对于 \(fa_x\) 的祖先来说,更新时每次向上跳的前提是上次修改的结点是当前结点的右儿子,否则便会结束向上更新。而初始时 \(dist\) 为 \(dist_{fa_x}\),每次向上跳都会使得 \(dist\) 增大,考虑到 \(dist\) 是 \(\mathcal O(\log)\) 级别的,故更新是 \(\mathcal O(\log n)\) 的,故删除任意结点的复杂度为 \(\mathcal O(\log n)\)。
整个堆加上/减去一个值、乘上一个正数
其实可以打标记且不改变相对大小的操作都可以。
在根打上标记即可,每次访问儿子时 pushdown。
判断两个元素所在堆是否已经合并
并查集维护即可。
已知初始 \(n\) 个元素,构建一颗左偏树可以做到 \(\mathcal O(n)\)
将每个结点放入双端队列,每次取队首两个元素进行合并,后将合并后元素插入队尾,可以证明时间时间复杂度为 \(\mathcal O(n)\)。
注意事项
删除根结点后,根结点的 fa 需要修改为合并左右子树之后的根,因为路径压缩后根节点的儿子对应的 fa 可能指向根节点。
#include <bits/stdc++.h>
const int N = 1e5 + 10;
inline int read()
{
int cnt = 0; char ch = getchar(); bool op = 1;
for (; ! isdigit(ch); ch = getchar())
if (ch == '-') op = 0;
for (; isdigit(ch); ch = getchar())
cnt = cnt * 10 + ch - 48;
return op ? cnt : - cnt;
}
int n, m;
int dist[N], val[N], lc[N], rc[N];
int fa[N];
inline int find(int x)
{
if (fa[x] != x) return fa[x] = find(fa[x]);
return fa[x];
}
inline int merge(int x, int y)
{
if (! x || ! y) return x + y;
if (val[x] > val[y] || (val[x] == val[y] && x > y))
std::swap(x, y);
rc[x] = merge(rc[x], y);
if (dist[rc[x]] > dist[lc[x]])
std::swap(lc[x], rc[x]);
dist[x] = dist[rc[x]] + 1;
return x;
}
int vis[N];
int main()
{
n = read(), m = read();
dist[0] = -1;
for (int i = 1; i <= n; ++ i)
{
fa[i] = i; val[i] = read();
}
for (int i = 1; i <= m; ++ i)
{
int op, x, y;
op = read();
if (op == 1)
{
x = read(), y = read();
int fax = find(x), fay = find(y);
if (fax != fay && vis[x] == 0 && vis[y] == 0)
{
int allfa = merge(fax, fay);
fa[fax] = fa[fay] = allfa;
}
}
if (op == 2)
{
x = read();
if (vis[x])
{
printf("-1\n");
}
else
{
int fax = find(x); dist[fax] = -1;
vis[fax] = 1;
printf("%d\n", val[fax]);
int allfa = merge(lc[fax], rc[fax]);
fa[fax] = fa[lc[fax]] = fa[rc[fax]] = allfa;
}
}
}
return 0;
}