一般的二叉堆,要对其进行合并操作,最朴素的方法是每个元素逐一与另一个堆合并,即使是启发式,也是 O(n)的复杂度。
然而,左偏树可以做到 O(log n),因为他并不是暴力合并,而是基于深度,而深度最多为 log。
这里,我们引入一个变量 dist,指的是这个节点到叶子节点的距离。特别地,叶子节点的 dist 为 0,空节点的 dist 为 -1。
证明:一个有 n 个节点的左偏树,根节点(也就是 dist 最大的节点)的 dist 不大于 log(n + 1) + 1。
假设根节点的 dist 为 k,那么树上最少有 2 ^ (k + 1) - 1 个节点,那么有 n >= 2 ^ (k + 1) - 1。
所以 log(n + 1) + 1 >= k 。
这样一来,复杂度基于高度的合并就是正确的了。
左偏树的核心操作在于合并,而合并操作的核心在于 merge() 函数。
merge() 函数的参数是两个待合并的树(当然也是左偏树)的根节点,而返回值是合并后新的根节点。
这是一种递归式的操作。
每次对比两个参数,令更小的(视情况而定)作为新的根节点,较大的根节点再与新根节点的右儿子合并(因为树是左偏的,右子树才是决定 dist 的子树,保证 log 级别的复杂度),反复递归。
P3377 【模板】左偏树/可并堆
点击查看代码
#include <bits/stdc++.h>
using namespace std;
template <typename T> inline void read(T &x)
{x = 0; bool f = 0; char ch = getchar();while('0' > ch || ch > '9') { if(ch == '-') f = !f; ch = getchar(); }while('0' <= ch && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }x = f ? -x : x; return;
}
template <typename T> inline void print(T x)
{if(x < 0) putchar('-'), x = -x;if(x > 9) print(x / 10);putchar(x % 10 + '0'); return;
}
const int N = 1e5 + 10;
int n, m;
int a[N], fa[N], lc[N], rc[N], dist[N], del[N];
int find(int x)
{if(fa[x] == x) return x;return fa[x] = find(fa[x]);
}
int merge(int x, int y)
{if(!x || !y) return x + y;if(a[x] > a[y] || a[x] == a[y] && x > y) swap(x, y);rc[x] = merge(rc[x], y);fa[rc[x]] = x;if(dist[lc[x]] < dist[rc[x]]) swap(lc[x], rc[x]);dist[x] = dist[rc[x]] + 1;return x;
}
void Pop(int x)
{del[x] = 1;fa[lc[x]] = lc[x], fa[rc[x]] = rc[x];fa[x] = fa[lc[x]] = fa[rc[x]] = merge(lc[x], rc[x]);lc[x] = rc[x] = dist[x] = 0;
}
int main()
{dist[0] = -1;read(n); read(m);for(int i = 1; i <= n; i ++) read(a[i]), fa[i] = i;int op, x, y;for(int i = 1; i <= m; i ++){read(op);if(op == 1){read(x); read(y);if(del[x] || del[y]) continue;x = find(x); y = find(y);if(x != y) fa[x] = fa[y] = merge(x, y);}else if(op == 2){read(x);if(del[x]) print(-1), putchar('\n');else{x = find(x);print(a[x]);putchar('\n');Pop(x);}}}return 0;
}