左偏树
一. 定义与性质
1.外节点: 一棵二叉树中左儿子或右儿子为空的节点称为外节点。
2.左偏树(Leftist Tree) 是一种可并堆的实现。左偏树是一棵二叉树,每个节点维护的值有:左右儿子,键值val和dist。
其中键值val用于比较节点的大小,dist表示此节点到其子树中最近的外节点的距离,用于维护左偏的性质。特别的,外节点的dist为0,空节点的dist为-1
左偏树的基本性质:
[堆性质] 一个节点的键值小于等于(或大于等于)其左右节点的键值
满足此条性质,我们就可以在\(O(1)\) 时间内完成取最小值或最大值的操作。
[左偏性质] 一个节点的左儿子的dist大于等于右儿子的dist
[性质3] 一个节点的dist等于其右儿子的dist + 1
[性质4] 一颗dist为\(k\)的左偏树至少有 \(2^{k + 1} - 1\) 个节点
推论:一颗n个节点的左偏树的dist最多为 \(\lfloor log(n + 1) - 1 \rfloor\)
二. 基本操作
合并操作(merge)
若不考虑左偏性质,合并两个堆(以小根堆为例)x, y分为以下几步:
1.交换x,y使得\(val_x < val_y\)
2.递归合并y与x的一个儿子,更新儿子信息
3.直到x,y其中一个为空停止
这样合并时间复杂度并不稳定,若合并的两个树都为链,时间复杂度将退化为\(O(n)\)
因此我们第二步合并时选择dist更小的x的右节点,设x和y的节点数分别为\(N_x\)和 \(N_y\), 由性质四的推论可知,这样合并的时间复杂度为 \(O(log N_x + log N_y)\)
合并之后我们需要继续维护左偏性质
int merge(int x, int y) {
if(!x || !y) return x + y; //其中一个为空,返回另一个
if(val[x] > val[y]) swap(x, y);
son[x][1] = merge(son[x][1], y); //x的值更小,把y合并到x的右子树上
fa[son[x][1]] = x; //根据题目确定是否需要并查集
if(dist[son[x][1]] > dist[son[x][0]]) swap(son[x][0], son[x][1]); //维护左偏的性质
dist[x] = dist[son[x][1]] + 1;
return x;
}
取最值
取出堆顶键值即可
插入值
将单个的节点看作一个堆进行合并操作
删除堆顶
将堆顶节点的左右儿子合并即可
删除任意节点
先将其左右儿子合并,然后向上更新dist,直到不需要更新
三.模板
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <map>
using namespace std;
const int N = 100005;
int son[N][2], dist[N], val[N], fa[N];
int n, m;
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(val[x] > val[y] || (val[x] == val[y] && x > y)) swap(x, y);
son[x][1] = merge(son[x][1], y); //x的值更小,把y合并到x的右子树上
fa[son[x][1]] = x;
if(dist[son[x][1]] > dist[son[x][0]]) swap(son[x][0], son[x][1]); //维护左偏的性质
dist[x] = dist[son[x][1]] + 1;
return x;
}
void pop(int x) {
val[x] = -1;
fa[son[x][0]] = fa[son[x][1]] = fa[x] = merge(son[x][0], son[x][1]);
}
int main() {
// freopen("data.in", "r", stdin);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d", &val[i]);
}
for(int i = 0; i <= n; i++) fa[i] = i;
dist[0] = -1;
for(int i = 1; i <= m; i++) {
int opt, x, y; scanf("%d", &opt);
if(opt == 1) {
scanf("%d%d", &x, &y);
if(val[x] == -1 || val[y] == -1) continue;
int fx = find(x), fy = find(y);
if(fx == fy) continue;
fa[fx] = fa[fy] = merge(fx, fy);
} else {
scanf("%d", &x);
int fx = find(x);
if(val[x] == -1) {
printf("-1\n"); continue;
}
printf("%d\n", val[fx]);
pop(fx);
}
}
return 0;
}
四.其他操作
整堆修改
类似于线段树的lazy标记,整堆进行加法或乘法修改时只需要修改堆顶元素,然后打上标记,在进行其他操作时将标记下传即可。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <map>
using namespace std;
typedef long long lld;
const int N = 300005;
struct Knight {
int st_dep; lld val;
};
struct Heap {
Knight kt;
int son[2], dist;
lld mul, delt;
} h[N];
vector <int> e[N];
int st[N], ans1[N], ans2[N], dep[N];
lld v[N], opt[N];
int opt_type[N], n, m;
void pushdown(int x) {
if(h[x].son[0]) {
h[h[x].son[0]].mul *= h[x].mul; h[h[x].son[0]].delt *= h[x].mul;
h[h[x].son[0]].delt += h[x].delt;
h[h[x].son[0]].kt.val *= h[x].mul; h[h[x].son[0]].kt.val += h[x].delt;
}
if(h[x].son[1]) {
h[h[x].son[1]].mul *= h[x].mul; h[h[x].son[1]].delt *= h[x].mul;
h[h[x].son[1]].delt += h[x].delt;
h[h[x].son[1]].kt.val *= h[x].mul; h[h[x].son[1]].kt.val += h[x].delt;
}
h[x].delt = 0; h[x].mul = 1;
}
int merge(int x, int y) {
if(!x || !y) return x + y;
pushdown(x); pushdown(y);
if(h[x].kt.val > h[y].kt.val) swap(x, y);
h[x].son[1] = merge(h[x].son[1], y);
if(h[h[x].son[0]].dist < h[h[x].son[1]].dist) swap(h[x].son[0], h[x].son[1]);
h[x].dist = h[h[x].son[1]].dist + 1;
return x;
}
void dfs1(int x) {
for(auto y : e[x]) {
dep[y] = dep[x] + 1;
dfs1(y);
}
}
void dfs(int x) {
int rt = st[x];
for(auto y : e[x]) {
dfs(y);
if(!st[y]) continue;
if(!rt) {
rt = st[y]; continue;
}
rt = merge(rt, st[y]);
}
while(h[rt].kt.val < v[x] && rt) {
ans1[x]++; ans2[rt] = h[rt].kt.st_dep - dep[x];
pushdown(rt);
h[rt].kt.val = -1;
rt = merge(h[rt].son[0], h[rt].son[1]);
}
st[x] = rt;
if(!st[x]) return ;
pushdown(rt);
if(!opt_type[x]) {
h[rt].delt += opt[x]; h[rt].kt.val += opt[x];
} else {
h[rt].mul *= opt[x]; h[rt].delt *= opt[x]; h[rt].kt.val *= opt[x];
}
}
int main() {
// freopen("data1.in", "r", stdin);
// freopen("data.out", "w", stdout);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%lld", &v[i]);
}
for(int i = 2, fa; i <= n; i++) {
scanf("%d%d%lld", &fa, &opt_type[i], &opt[i]);
e[fa].push_back(i);
}
dfs1(1); h[0].dist = -1;
for(int i = 1; i <= m; i++) {
lld start; int pos; scanf("%lld%d", &start, &pos);
h[i].kt = (Knight){dep[pos], start};
h[i].mul = 1;
if(st[pos]) st[pos] = merge(st[pos], i);
else st[pos] = i;
}
dfs(1);
for(int i = 1; i <= n; i++) {
printf("%d\n", ans1[i]);
}
for(int i = 1; i <= m; i++) {
if(h[i].kt.val != -1) ans2[i] = h[i].kt.st_dep + 1;
printf("%d\n", ans2[i]);
}
return 0;
}
五.其他例题
论文题:P4331 [BalticOI 2004]Sequence 数字序列
《黄源河 -- 左偏树的特点及其应用》