算法学习笔记(12):左偏树
左偏树
定义:
- 左偏树是一个堆, 具有堆的性质, 并且是
左偏的 - 如果一个点只有左儿子或右儿子, 那么就称它为外节点, 定义一个结点的 \(dist\) 等于这个点到最近外节点的距离加 \(1\) 。特殊的, 外节点 \(dist\) 等于 \(1\) 。
- 左偏性质: 每个节点的左儿子 \(dist\) >= 右儿子 \(dist\)。
操作
左偏树最重要的操作就是 合并(\(merge\))操作, 有了这个操作, 其他操作都很简单, 和 FHQtreap 有一点像。
先看代码吧。
int merge(int x, int y) {
if (!x || !y) return x | y;
if (v[y] < v[x]) swap(x, y);
rs[x] = merge(rs[x], y);
if (dist[ls[x]] < dist[rs[x]]) swap(ls[x], rs[x]);
dist[x] = dist[rs[x]] + 1;
return x;
}
\(merge\) 操作主要注意两点:
- 维护左偏性质。 2. 维护堆性质。
大根堆和小根堆没什么区别, 所以接下来都按小根堆来讲。
所以合并两个堆的时候, 取较小的根作为新根, 并且保留它的左儿子作为新堆的左儿子, 将它的右儿子和另一个堆递归合并作为新堆的右儿子。
可以看出每次递归, 右儿子 \(dist\) 减 \(1\), 所以并且根据定义, 如果这个堆大小为 \(n\), 则根的 \(dist\) 不会大于 \(logn\), 所以最多递归 $ O(logn) $ 层,时间复杂度就得到保证。
其他操作就很ez了。
例题
P2713 罗马游戏
模板题。两个操作。
- 合并两个堆。
- 删掉某个点所在堆的堆顶。
多了一个找根的操作, 不能暴力跳, 因为左偏树树高可以到达到 \(O(n)\) , 暴力跳t飞了, 不过可以并查集, 路径压缩一下时间复杂度 \(O(并查集)\) , 不过合并和删点的时候更新 \(fa\) 数组要注意, 很容易错。
删点的操作就直接合并它的左右儿子就行了。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, m, fa[N], fl[N];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
struct head{
int ls[N], rs[N], dis[N], val[N];
int merge(int x, int y) {
if (!x || !y) return x | y;
if (val[x] > val[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;
}
void del(int x) {
int rt = merge(ls[x], rs[x]);
fa[x] = fa[ls[x]] = fa[rs[x]] = rt;
dis[x] = ls[x] = rs[x] = 0;
}
}H;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &H.val[i]);
fa[i] = i;
}
scanf("%d", &m);
char op[5];
for (int i = 1, x, y; i <= m; i++) {
scanf("%s%d", op, &x);
if (*op == 'M') {
scanf("%d", &y);
if (fl[x] || fl[y]) continue;
x = find(x), y = find(y);
if (x == y) continue;
fa[x] = fa[y] = H.merge(x, y);
}
if (*op == 'K') {
if (fl[x]) printf("0\n");
else {
x = find(x);
printf("%d\n", H.val[x]);
H.del(x);
fl[x] = 1;
}
}
}
return 0;
}

浙公网安备 33010602011771号