参考博客:
左偏树的应用:
左偏树是可并堆的一种实现。它满足堆的性质,并且能在log的时间复杂度下合并两棵左偏树。
左偏树的定义:
定义1 左偏树中的一个节点,如果它的右子树为空,则称它是一个外结点。
定义2 对于左偏树中的一个节点x,到它的子节点中,离它最近的一个外结点经过的边数称为它的距离,记为dist(x)。特别地,外结点的距离为0,空节点(null)的距离为-1。
现在约定一下,本文中出现的左偏树的距离是指,左偏树根节点的距离。
性质1(堆性质) 对于左偏树中的一个非叶节点应满足堆的性质。如果是大根堆,应满足任意非叶节点的左子树和右子树(如果有的话)的根节点的权值大于等于这个节点,即。如果是小根堆则满足应满足任意非叶节点的左子树和右子树(如果有的话)的根节点的权值小于等于这个节点。
性质2(左偏性质) 对于左偏树中的任意节点满足它的左子树的距离大于等于右子树的距离。即。
性质3 左偏树中的任意节点的左子树和右子树(如果有的话)都是左偏树。
由这几条性质可以得出左偏树的定义:左偏树是具有左偏性质的堆有序二叉树。
左偏树性质:
引理1 左偏树中的节点的距离总是满足。
证明 根据定义2有。根据左偏性质可以证得。
现在就来探讨一下当左偏树的距离为 时,左偏树的节点数至少为多少。我们希望节点数尽量少,那应该满足
,根据引理1可以得到它们都等于
。此时的左偏树就是一棵深度为
,所以此时的左偏树的节点个数为
。整理得到下面的结论。
定理2 所有距离为k的左偏树中,节点最少的是满二叉树,且节点数为。
当我知道一棵左偏树有n个节点,我能否计算出它的最大深度?答案是肯定的。
推论3 一棵节点数为n的左偏树,它的距离至多为。
证明 根据定理2有。因此
。因为k为整数,所以
。因此定理得证。
现在再做一个临时的约定,称从左偏树根节点一直访问右子树,直到不能访问位置所有经过的边和点形成的链为最右链。
引理4 左偏树的最右链恰好有1个外结点。
证明 显然存在一个外结点,不然这一定不是一棵树。假设存在两个,对于深度浅一点的外结点它存在右子树(不然没有后文了),但是不存在左子树(不然它不是外结点),与左偏性质矛盾,所以定理得证。
定理5 一棵有n个节点的左偏树的最右链,至多有个节点。
证明 根据推论3有最右链的至多有个非外节点,又根据引理4,可以得知最右链至多有
个节点。因此定理得证。
模板:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX = 1e5 + 10;
//左偏树结构体定义
struct tree {
int l, r;
int fa, dis, val;
}tr[MAX];
int n, m;
//并查集
int finds(int x)
{
return tr[x].fa == -1 ? x : tr[x].fa = finds(tr[x].fa);
}
//合并
//插入操作相当于一个节点的左偏树和另一棵左偏树进行合并
int merge(int x, int y)
{
if (x == 0 || y == 0) return x + y; //如果为0的话,就说明是空子树,根节点当然就是另一节点了
if (tr[y].val > tr[x].val) swap(x, y); //始终往右子树进行插入
tr[x].r = merge(tr[x].r, y);
tr[tr[x].r].fa = x;
if (tr[tr[x].l].dis < tr[tr[x].r].dis) swap(tr[x].l, tr[x].r); //是否需要左右子树的对换,这样是为了右子树尽量短
if (tr[x].r == 0) tr[x].dis = 0; //距离的重新分配
else tr[x].dis = tr[tr[x].r].dis + 1;
return x;
}
//删除
int del(int root)
{
int l = tr[root].l;
int r = tr[root].r;
tr[root].l = tr[root].r = tr[root].dis = 0;
tr[root].fa = -1;
tr[l].fa = tr[r].fa = -1; //删除root根节点
return merge(l, r); //这样一来相当于分裂成了两棵子树,重新进行合并,最后返回值为合并后的根节点
}
//初始化
void init()
{
for (int i = 1; i <= n; i++) {
tr[i].l = tr[i].r = tr[i].dis = 0;
tr[i].fa = -1;
}
}
例题:
题意:
有n只猴子,每只猴子都有一个能力值,一开始互不认识,当两只不认识的猴子碰到一起,他们就会分别找他们朋友里最强的猴子来和对面决斗,一次决斗后,决斗的两只猴子能力值都除以2,打完一次后,这两派猴子就都是朋友了。现在有m个询问,每次给定两只猴子x,y,如果x,y是一派的,就输出-1,否则就按上述规则进行决斗,并输出决斗完,他们这一派中的能力最大值。
思路:
左偏树模板题。用并查集维护它们是不是一派的,左偏树维护大根堆。如果不是一派的,那么就用x,y所在左偏树的根节点(能力最大值)来互相决斗,决斗完,其能力值/2,先在它们对应的左偏树中删掉它们,再插入(维护左偏树的性质)。最后再进行合并。输出合并后根节点的能力值即可。
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX = 1e5 + 10;
struct tree {
int l, r;
int fa, dis, val;
}tr[MAX];
int n, m;
int finds(int x)
{
return tr[x].fa == -1 ? x : tr[x].fa = finds(tr[x].fa);
}
int merge(int x, int y)
{
if (x == 0 || y == 0) return x + y;
if (tr[y].val > tr[x].val) swap(x, y);
tr[x].r = merge(tr[x].r, y);
tr[tr[x].r].fa = x;
if (tr[tr[x].l].dis < tr[tr[x].r].dis) swap(tr[x].l, tr[x].r);
if (tr[x].r == 0) tr[x].dis = 0;
else tr[x].dis = tr[tr[x].r].dis + 1;
return x;
}
int del(int root)
{
int l = tr[root].l;
int r = tr[root].r;
tr[root].l = tr[root].r = tr[root].dis = 0;
tr[root].fa = -1;
tr[l].fa = tr[r].fa = -1;
return merge(l, r);
}
int main()
{
while (scanf("%d", &n) != EOF)
{
for (int i = 1; i <= n; i++) {
tr[i].l = tr[i].r = tr[i].dis = 0;
tr[i].fa = -1;
scanf("%d", &tr[i].val);
}
scanf("%d", &m);
while (m--) {
int x, y;
scanf("%d%d", &x, &y);
int fx = finds(x);
int fy = finds(y);
if (fx == fy) {
printf("-1\n");
continue;
}
tr[fx].val /= 2;
//插入操作相当于一个节点的左偏树和另一棵左偏树进行合并
int xx = merge(del(fx), fx);
tr[fy].val /= 2;
int yy = merge(del(fy), fy);
printf("%d\n", tr[merge(xx, yy)].val);
}
}
return 0;
}