「笔记」左偏树
写在前面
左偏树是在二叉树的结构上进行维护的,在这个二叉树中它满足堆的性质。
特殊的是,左偏树可以在 \(O(\log_2 n)\) 的时间内进行合并操作。
下面的讲解默认左偏树为小根堆
正文
一些定义
- 外结点:左儿子或右儿子为空的结点。
- 距离 \(dis_i\) : 表示结点 \(i\) 到 外结点的最短距离。特别的空结点的距离为 \(-1\)
- \(lson/rson\):左右儿子、
- \(val_i\):结点 \(i\) 的权值。
基本性质
堆性质:对于每个节点 \(x\) ,满足 \(val_x < val_{lson_x}, val_x < val_{rson_x}\)。注意这里 \(val_{lson_x}\) 和 \(val_{rson_x}\) 的大小关系并不能确定。
左偏性质:对于每个节点 \(x\) ,有 \(dis_{lson_x} > dis_{rson_x}\)。
当然反过来就可以叫右偏树了
几个结论
-
结点 \(x\) 的距离 \(dis_x = dis_{rson_x} + 1\)。
-
距离为 \(n\) 的左偏树至少有 \(2^{n+1}-1\) 个结点,此时它的形态是一个满二叉树。
-
有 \(n\) 个结点的左偏树树高为 \(\log_2 n\),可由第二个结论推导而来。
核心操作-合并操作
定义 Merge(x,y)
为合并两个分别以 \(x,y\)为根的左偏树,返回值为新根。
首先不考虑左偏树的性质,考虑合并两个具有堆性质的二叉树。默认为小根堆。
1、如果 \(val_x < val_y\) ,根节点为 \(x\),否则为 \(y\),为了避免分类讨论可以将 \(x,y\) 交换。
2、将 \(y\) 与 \(x\) 的一个儿子合并,用合并后的根节点代替 \(x\) 的这个儿子的位置。
3、递归的进行上述过程,如果 \(x\) 或 \(y\) 为空节点,返回 \(x+y\)。
设树高为 \(h\) ,每次合并 \(h_x + h_y\) 都会减少 \(1\) ,所以复杂度是 \(O(h)\) 的,知道刻意造一下数据,使其合并后退化为一条链,可以把每次合并卡成 \(O(n)\) 的。
这显然不是我们想要的,考虑怎么合并让他变得更加平衡?
利用 \(FHQ-Treap\) 的思想每次随机选择一个结点合并? 应该是可以的。
但是我们左偏树的性质还没用啊。
因为左偏树中 左儿子的距离大于右儿子的距离,这说明右子树结点数更小,所以我们 每次将 \(y\) 与 \(x\) 的右儿子合并。
最后总的树高 \(h = \log_2 n\),每次合并的复杂度为 \(O(\log_2 n)\)。
注意一次合并完可能不在满足左偏树的性质。这时候我们把左右儿子交换就好了。
至于 \(dis_x\),显然初始化时都是 \(0\),合并完根据上面的第一个推论更新 \(dis_x\) 的值即可。
下面结合代码理解。
Code
namespace LIT {
#define ls lson[x]
#define rs rson[x]
int lson[MAXN], rson[MAXN], fa[MAXN], dis[MAXN];
bool vis[MAXN];
struct node { // 如果题目没有要求直接开 int 也可以。
int pos, val;
bool operator < (const node &b) {
return val == b.val ? pos < b.pos : val < b.val;
}
}val[MAXN];
int Find(int x) { return fa[x] == x ? x : fa[x] = Find(fa[x]); } // 路径压缩
int Merge(int x, int y) {
if(!x || !y) return x + y;
if(val[y] < val[x]) swap(x, y);
rs = Merge(rs, y);
if(dis[ls] < dis[rs]) swap(ls, rs);
dis[x] = dis[rs] + 1;
return x;
}
}
基操-插入一个新的结点
新建一个结点然后执行 Merge
操作即可
基操-找一个结点的根节点
不断跳 \(fa\) 即可。
可能会太慢。
路径压缩!
具体原理和并查集相同。
基操-求最小值
默认小跟堆,返回根节点对应权值即可。
大根堆同理。
基操-删除一个最小值
把根节点架空,合并两个子树即可。
注意路径压缩带来的影响,所以合并的时候要让三者的 \(fa\) 都指向新的根节点。
同时注意标记已删除的节点,清除已删除的节点的信息。
假设根节点为 \(x\):
fa[lson[x]] = fa[rson[x]] = fa[x] = Merge(lson[x], rson[x]);
vis[x] = true; // 标记已被删除
lson[x] = rson[x] = dis[x] = 0; // 清除信息
例题
P3377 【模板】左偏树(可并堆)
模板题。
P2713 罗马游戏
模板题。
P1456 Monkey King
简述题意:一开始有 \(n\) 个猴子,猴子有强壮值,进行 \(m\) 次合并,合并后两个猴子属于一个群体。每次合并给你两个猴子编号,需要先将这两个猴子所在群体中的猴王(最强的)的强壮值减半,然后进行合并。
Solution:
利用左偏树的性质进行模拟即可。
对于每个群体,我们可以先把它的猴王删掉,然后更改猴王的强壮值,再把它合并进来。
然后直接合并两个群体就做完了。
修改猴王的强壮值要直接在它对应的结点修改。
如果新建结点的话会出现一些奇怪的错误,目前不清楚,可以看一下KnightL 的帖子。
大概是因为关系紊乱?毕竟猴王只是不那么强壮了而不是挂掉了,也不可能是生出一个小猴王。
Code
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
int n, m;
int read(){
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
namespace LIT {
#define ls lson[x]
#define rs rson[x]
int fa[MAXN], lson[MAXN], rson[MAXN], dis[MAXN];
struct node {
int pos, val;
bool operator < (const node &b) {
return val == b.val ? pos > b.pos : val > b.val;
}
}val[MAXN];
int Find(int x) { return fa[x] == x ? x : Find(fa[x]); }
void Clear() {
memset(lson, false, sizeof lson);
memset(rson, false, sizeof rson);
memset(dis, false, sizeof dis);
memset(fa, false, sizeof fa);
}
int Merge(int x, int y) {
if(!x || !y) return x + y;
if(val[y] < val[x]) swap(x, y);
rs = Merge(rs, y);
if(dis[ls] < dis[rs]) swap(ls, rs);
dis[x] = dis[rs] + 1;
return x;
}
}
using namespace LIT;
int main()
{
while(cin >> n) {
Clear();
for(int i = 1; i <= n; ++i) fa[i] = val[i].pos = i, val[i].val = read();
m = read();
for(int i = 1, u, v; i <= m; ++i) {
u = read(), v = read();
int uf = Find(u), vf = Find(v);
if(uf == vf) puts("-1");
else {
int x = fa[lson[uf]] = fa[rson[uf]] = fa[uf] = Merge(lson[uf], rson[uf]);
int y = fa[lson[vf]] = fa[rson[vf]] = fa[vf] = Merge(lson[vf], rson[vf]);
lson[uf] = rson[uf] = dis[uf] = 0;
lson[vf] = rson[vf] = dis[vf] = 0;
val[uf].val /= 2, val[vf].val /= 2;
fa[uf] = uf, fa[vf] = vf;
x = fa[x] = fa[uf] = Merge(x, uf);
y = fa[y] = fa[vf] = Merge(y, vf);
fa[x] = fa[y] = Merge(x, y);
printf("%d\n", val[fa[x]].val);
}
}
}
return 0;
}