写在前面
左偏树是在二叉树的结构上进行维护的,在这个二叉树中它满足堆的性质。
特殊的是,左偏树可以在 (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) 的值即可。
下面结合代码理解。
基操-插入一个新的结点
新建一个结点然后执行 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; // 清除信息
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;
}
}
例题
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
", val[fa[x]].val);
}
}
}
return 0;
}