左偏树学习笔记
左偏树 (Leftist Heap) 是一种可并堆,他支持在 \(O(\log n)\) 的时间内将两个堆合并。
具体来说,左偏树除了有堆的性质(节点小于儿子),还额外维护一个值 \(dist\),表示当前节点最少经过多少条边可以到一个叶子节点。这里定义叶子节点的 \(dist=0\),空节点 \(dist=-1\)。
\(dist\) 的性质是:
- 对于一个节点 \(x\),\(dist(x)=dist(rson)+1\)。
- 任意一棵左偏树的 \(dist\) 不会超过 \(\log N\),其中 \(N\) 是左偏树节点个数。
证明:
- 由 \(dist\) 的定义可知 \(dist(x)=\min(dist(lson), dist(rson))+1\),又因为 \(dist(lson)\ge dist(rson)\),故 \(dist(x)=dist(rson)+1\)。
- 考虑一棵 \(dist=k\) 的左偏树最少有多少个节点,不放令这个值为 \(S(k)\)。根据第一条性质,我们知道 \(dist(x)=dist(rson)+1\),故其右儿子至少有 \(S(k-1)\) 个节点,左儿子也应该至少有 \(S(k-1)\) 个节点,故总结点个数 \(S(k)=2\times S(k-1)+1\)。由于边界情况 \(S(0)=1\),故 \(S(k)=2^{k+1}-1\)。即 \(k\le \log_2(N+1)-1\)。
每次合并两个堆的时候,设他们的根节点为 \(x,y\) 且 \(val(x)<val(y)\),我们递归的把 \(x\) 的右儿子与 \(y\) 合并,然后再把这个合并后的堆当做 \(x\) 的右儿子。如果此时 \(dist(rson)>dist(lson)\),那么就交换左右儿子。
时间复杂度证明:
- 每次合并后,得到的一个新的堆仍然满足左偏树的性质。
- 递归的次数等于右儿子的个数,也就是 \(dist(x)\)。
故时间复杂度为 \(O(\log n)\)。
const int MAXN = 1e5 + 5;
struct Node {
int val, dist, idx;
Node *ls, *rs;
bool operator < (const Node b) const {
return val == b.val ? idx < b.idx : val < b.val;
}
};
Node *rt[MAXN];
int n, m, fa[MAXN], del[MAXN];
int find(int x) {
if (fa[x] == x) return x;
else return fa[x] = find(fa[x]);
}
int dist(Node *x) {
if (x == nullptr) return -1;
else return x->dist;
}
Node* merge(Node* x, Node* y) {
if (x == nullptr) return y;
if (y == nullptr) return x;
if (*y < *x) swap(x, y);
x->rs = merge(x->rs, y);
if (dist(x->ls) < dist(x->rs)) swap(x->ls, x->rs);
x->dist = dist(x->rs) + 1;
return x;
}
void work() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
int x; cin >> x;
rt[i] = new Node({x, 0, i, nullptr, nullptr});
fa[i] = i;
}
while (m--) {
int op; cin >> op;
if (op == 1) {
int x, y; cin >> x >> y;
if (del[x] || del[y]) continue;
if (find(x) == find(y)) continue;
rt[find(x)] = merge(rt[find(x)], rt[find(y)]);
fa[find(y)] = find(x);
} else {
int x; cin >> x;
if (del[x]) {
cout << -1 << endl;
continue;
}
cout << rt[find(x)]->val << endl;
del[rt[find(x)]->idx] = true;
rt[find(x)] = merge(rt[find(x)]->ls, rt[find(x)]->rs);
}
}
}

浙公网安备 33010602011771号