左偏树学习笔记
具体印象
普通的二叉堆只能实现普通的堆的功能。
如果需要支持合并操作,就需要使用可并堆,左偏树就是一种好写的可并堆。
左偏树是一颗二叉树,由于特殊性质,整棵树会集中在左边,所以叫做左偏树。
左偏给堆的合并创造了很优秀的复杂度。
上面的图片来自luoguP3378题解区@lolte dalao。随便盗来的
定义
左偏树有一个npl值,但是我不习惯这么叫,直接把它叫做dist。
定义0节点的dist值为-1。同时定义每一个节点的dist值为它右儿子的dist值\(+1\)。
上面那张图,节点外的数字就是该节点的dist值。
可以发现,左偏树的dist值都出奇的小,结合下面的核心操作,你就知道怎么搞出这么优秀的复杂度了。
核心操作:合并(merge)
左偏树有且仅有这么一个操作:合并(merge)。
合并操作比较简单:递归弄到主堆的最右节点,与次堆合并,之后再判断左右子树的dist值大小,把dist值大的子树作为左子树,满足其左偏的性质。
上面的图片来自luoguP3377题解区@远航之曲 dalao。乱盗图系列
核心代码直接po上来:
int merge(int x, int y) {
if(x == 0 || y == 0) return x + y;// 只要一个为空,就返回另外一个
if(val[x] > val[y]) std::swap(x, y);// 满足小根堆的性质(因题而异)
else if(val[x] == val[y] && x > y) std::swap(x, y);// 满足下标小的先(题目特殊要求)
ch[x][1] = merge(ch[x][1], y);// 递归用右子树合并
fa[ch[x][1]] = x;// 认爸爸
if(dist[ch[x][0]] < dist[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);// 满足左偏树性质
dist[x] = dist[ch[x][1]] + 1;// 根据定义计算dist
return x;
}
其他操作
其他操作都是建立在merge操作之上的。比如:
在原堆上添加一个新数
把这个新数视为一个堆,直接用merge操作跟原堆合并即可。
删除堆顶
只需要获得堆顶,让它两个儿子都不认它作爸爸,把这两个新堆一起合并即可。
注意事项
操作模板是给你\(n\)个堆,再进行这些堆的操作,而不是给你插入\(n\)个数。这个是要注意的。
左偏树的路径压缩问题
没有路径压缩的左偏树在P3377是会T最后一个点的。
但是实际发现好多可并堆的题并不卡不路径压缩的代码。
如果要路径压缩也不是问题,但是需要更改一下上面fa
数组的定义了。
直接从find函数就可以明白改在哪里了:
int find(int x) {// 原
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
int find(int x) {// 新
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
其实就是一个并查集。
下面是有路径压缩的左偏树,与没有路径压缩的代码不同的地方已经标出来了:
/*************************************************************************
@Author: Garen
@Created Time : Mon 04 Feb 2019 11:14:55 AM CST
@File Name: P3377.cpp
@Description:
************************************************************************/
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
#define ll long long
const int maxn = 100005;
int dist[maxn], ch[maxn][2], fa[maxn], val[maxn];
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 == 0 || y == 0) return x + y;
if(val[x] > val[y]) std::swap(x, y);
else if(val[x] == val[y] && x > y) std::swap(x, y);
ch[x][1] = merge(ch[x][1], y);
if(dist[ch[x][0]] < dist[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);
fa[x] = fa[ch[x][0]] = fa[ch[x][1]] = x;// 三个节点的fa都是x
dist[x] = dist[ch[x][1]] + 1;
return x;
}
void pop(int x) {
val[x] = -1;
fa[ch[x][0]] = ch[x][0]; fa[ch[x][1]] = ch[x][1];// fa是它们自己
fa[x] = merge(ch[x][0], ch[x][1]);// merge的返回值需要用到
}
int main() {
cin >> n >> m;
dist[0] = -1;
for(int i = 1; i <= n; i++) {
cin >> val[i];
fa[i] = i;// 并查集的初始化
}
while(m--) {
int opt, x, y; cin >> opt;
if(opt == 1) {
cin >> x >> y;
if(val[x] == -1 || val[y] == -1) continue;
x = find(x), y = find(y);
if(x == y) continue;
fa[x] = fa[y] = merge(x, y);// merge的返回还是要用到
} else if(opt == 2) {
cin >> x;
if(val[x] == -1) cout << -1 << endl;
else {
x = find(x);
cout << val[x] << endl;
pop(x);
}
}
}
return 0;
}
练习题
1. P3378 【模板】堆
可并堆可以实现普通堆的操作,我自己认为还比二叉堆好写点。
唯一要注意的就是要yy出一个新堆,跟自己维护出的主堆合并就是了。
代码走一波:(没有路径压缩)
/*************************************************************************
@Author: Garen
@Created Time : Mon 04 Feb 2019 12:30:20 PM CST
@File Name: P3378.cpp
@Description:
************************************************************************/
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
const int maxn = 1000005;
int dist[maxn], fa[maxn], ch[maxn][2], val[maxn];
int n, m;
int mian;
int find(int x) {
while(fa[x]) x = fa[x];
return x;
}
int merge(int x, int y) {
if(x == 0 || y == 0) return x + y;
if(val[x] > val[y]) std::swap(x, y);
else if(val[x] == val[y] && x > y) std::swap(x, y);
ch[x][1] = merge(ch[x][1], y);
fa[ch[x][1]] = x;
if(dist[ch[x][0]] < dist[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);
dist[x] = dist[ch[x][1]] + 1;
return x;
}
int main() {
dist[0] = -1;
cin >> m;
for(int i = 1; i <= m; i++) {
int opt, x;
cin >> opt;
if(opt == 1) {
cin >> x;
val[++n] = x;
mian = merge(mian, n);
} else if(opt == 2) {
cout << val[mian] << endl;
} else if(opt == 3) {
val[mian] = -1;
fa[ch[mian][0]] = fa[ch[mian][1]] = 0;
mian = merge(ch[mian][0], ch[mian][1]);
}
}
return 0;
}
2. P1456 Monkey King
文体两开花
显然把每个孤独的猴子看成堆,每次就合并咯。这就是可并堆。
这里就有一个问题:战斗力减半如何操作?
其实也非常暴力:把战斗的猴子先删了,再插入一个减半的战斗力即可。
代码走一波:(没有路径压缩)
/*************************************************************************
@Author: Garen
@Created Time : Mon 04 Feb 2019 02:16:41 PM CST
@File Name: P1456.cpp
@Description:
************************************************************************/
#include<bits/stdc++.h>
#define ll long long
const int maxn = 100005;
int val[maxn], dist[maxn], fa[maxn], ch[maxn][2];
int n, m;
void clearlove() {
memset(dist, 0, sizeof dist);
memset(fa, 0, sizeof fa);
memset(ch, 0, sizeof ch);
}
int find(int x) {
while(fa[x]) x = fa[x];
return x;
}
int merge(int x, int y) {
if(x == 0 || y == 0) return x + y;
if(val[x] < val[y]) std::swap(x, y);
else if(val[x] == val[y] && x < y) std::swap(x, y);
ch[x][1] = merge(ch[x][1], y);
fa[ch[x][1]] = x;
if(dist[ch[x][0]] < dist[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);
dist[x] = dist[ch[x][1]] + 1;
return x;
}
int fight(int x, int y) {
x = find(x), y = find(y);
if(x == y) return -1;
int temp1 = val[x], temp2 = val[y];
val[x] >>= 1;
fa[ch[x][0]] = fa[ch[x][1]] = 0;
int temp_1 = merge(ch[x][0], ch[x][1]);
ch[x][0] = ch[x][1] = 0;
int newtemp_1 = merge(temp_1, x);
val[y] >>= 1;
fa[ch[y][0]] = fa[ch[y][1]] = 0;
int temp_2 = merge(ch[y][0], ch[y][1]);
ch[y][0] = ch[y][1] = 0;
int newtemp_2 = merge(temp_2, y);
int sb = merge(newtemp_1, newtemp_2);
return val[sb];
}
int main() {
while(scanf("%d", &n) == 1) {
clearlove();
dist[0] = -1;
for(int i = 1; i <= n; i++) scanf("%d", &val[i]);
scanf("%d", &m);
while(m--) {
int x, y; scanf("%d%d", &x, &y);
printf("%d\n", fight(x, y));
}
}
return 0;
}
最后
Happy Spring Festival!