左偏树
简介
可并堆是一种可以比较高效的实现的优先队列的合并操作的数据结构。
左偏树是可并堆最常见的一种。
相关定义
外结点:左子树或右子树为空的结点。
结点的距离:某个点和其子树的最近外结点的边数。其中外结点距离为
0
0
0,空结点距离为
−
1
-1
−1。
左偏树的距离:根结点的距离。
性质 1:堆有序,
val
(
x
)
⩽
val
(
l
s
o
n
x
)
,
val
(
x
)
⩽
val
(
r
s
o
n
x
)
\operatorname{val}(x) \leqslant \operatorname{val}(lson_x), \operatorname{val}(x) \leqslant \operatorname{val}(rson_x)
val(x)⩽val(lsonx),val(x)⩽val(rsonx)。
性质 2:左偏性质,
dis
(
l
s
o
n
x
)
⩾
dis
(
r
s
o
n
x
)
\operatorname{dis}(lson_x) \geqslant \operatorname{dis}(rson_x)
dis(lsonx)⩾dis(rsonx)。
性质 3:
dis
(
x
)
=
dis
(
r
s
o
n
x
)
+
1
\operatorname{dis}(x) = \operatorname{dis}(rson_x) + 1
dis(x)=dis(rsonx)+1。
性质 4:如果一棵左偏树的距离为
k
k
k,则该左偏树的结点数至少为
2
k
+
1
−
1
2^{k+1} - 1
2k+1−1。
性质 5:如果一棵左偏树的结点数为
n
n
n,则距离不超过
⌊
log
(
n
+
1
)
⌋
−
1
\lfloor\log(n + 1)\rfloor- 1
⌊log(n+1)⌋−1。
相关操作
合并
参考 Treap 的合并操作
得到两棵树的树根,我们考虑总是将其中一棵树并入另一棵树的右儿子,由此递归下去完成合并操作。
递归的时候同时要保证左偏树应满足的性质,所以我们有以下合并思路(以小根堆左偏树为例):
- 有两棵树 T x , T y T_x,T_y Tx,Ty ,其树根分别是 x , y x,y x,y,如果其中一棵为空,则返回另一棵
- 令 T x , T y T_x,T_y Tx,Ty 满足 val ( x ) ⩽ val ( y ) \operatorname{val}(x) \leqslant \operatorname{val}(y) val(x)⩽val(y),然后将 T y T_y Ty 与 T x T_x Tx 的右儿子合并(开始递归),得到新的 T x T_x Tx 的右儿子
- 为了保证其左偏性质,考虑 T x T_x Tx 的左右儿子是否需要互换位置
- 由性质3得,令 dis ( x ) = dis ( r s o n x ) + 1 \operatorname{dis}(x) = \operatorname{dis}(rson_x) + 1 dis(x)=dis(rsonx)+1
- 返回这棵新树 T x T_x Tx
int Merge(int x,int y)
{
if(!x || !y) return x + y;
if(t[x].val > t[y].val) swap(x,y);//保证性质1
t[x].rc = Merge(t[x].rc,y);
t[x].fa = t[t[x].lc].fa = t[t[x].rc].fa = x;
if(t[t[x].lc].dis < t[t[x].rc].dis) swap(t[x].lc,t[x].rc);//保证性质2
t[x].dis = t[t[x].rc].dis + 1;//利用性质3
return x;
}
考虑合并只会经过右儿子连成的链,由前面的性质得到链长
度不超过
⌊
log
(
dis
(
x
)
+
1
)
⌋
−
1
+
⌊
log
(
dis
(
y
)
+
1
)
⌋
−
1
⌊\log(\operatorname{dis}(x) + 1)⌋ − 1 + ⌊\log(\operatorname{dis}(y) + 1)⌋ − 1
⌊log(dis(x)+1)⌋−1+⌊log(dis(y)+1)⌋−1,则复
杂度为
O
(
log
n
)
O(\log n)
O(logn)。
删除某一结点
用分裂合并的思想可以得到如下步骤:
- 令 T L , T R T_L,T_R TL,TR 分别为 T x T_x Tx 的左子树和右子树
- 令 T x ′ T_{x'} Tx′ 为 T L T_L TL 和 T R T_R TR 合并后的新树
- 令 x x x 的父节结点标记为 T x ′ T_{x'} Tx′ 的根结点 x ′ x' x′ (防止寻根操作时出错)
- 将 T x ′ T_{x'} Tx′ 接入原树代替 T x T_{x} Tx 并向上回溯更新 dis \operatorname{dis} dis
寻根
用并查集优化达到压缩路径的目的,减少回溯的时间复杂度
int Getrt(int x)
{
if(t[x].fa == x) return x;
else
{
t[x].fa = Getrt(t[x].fa);
return t[x].fa;
}
}
相关例题
P3377 【模板】左偏树(可并堆)
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
int n,m,x,y;
int rt,op;
/*
外结点:左子树或右子树为空的结点。
结点的距离:某个点和其子树的最近外结点的边数。其中外结点距离为 0,空结点距离为 -1。
左偏树的距离:根结点的距离。
*/
struct node{
/*
左偏树的每个结点记录的信息有权值 val, 距离 dis, 左儿子ls, 右儿子 rs。
*/
int fa;
int dis;int val;
int lc;int rc;
}t[100015];
/*
性质 1:堆有序, val(x) ≤ val(ls(x)), val(x) ≤ val(rs(x))。
性质 2:左偏性质, dis(ls(x)) ≥ dis(rs(x))。
性质 3: dis(x) = dis(rs(x)) + 1。
性质 4:如果一棵左偏树的距离为 k,则该左偏树的结点数至少为 2^(k+1) - 1。
性质 5:如果一棵左偏树的结点数为 n,则距离不超过└log(n + 1)┘- 1。
*/
int Getrt(int x)
{
if(t[x].fa == x) return x;
else
{
t[x].fa = Getrt(t[x].fa);
return t[x].fa;
}
}
int Merge(int x,int y)
{
if(!x || !y) return x + y;
if(t[x].val > t[y].val) swap(x,y);//保证性质1
else if(t[x].val == t[y].val && x > y) swap(x,y);
t[x].rc = Merge(t[x].rc,y);
t[x].fa = t[t[x].lc].fa = t[t[x].rc].fa = x;
if(t[t[x].lc].dis < t[t[x].rc].dis) swap(t[x].lc,t[x].rc);//保证性质2
t[x].dis = t[t[x].rc].dis + 1;//利用性质3
return x;
}
int main()
{
scanf("%d%d",&n,&m);
t[0].dis = -1;
for(int i = 1;i <= n;i ++)
{
scanf("%d",&t[i].val);
t[i].dis = 0;t[i].fa = i;
}
for(int i = 1;i <= m;i ++)
{
scanf("%d",&op);
if(op == 1)
{
scanf("%d%d",&x,&y);
if(t[x].dis == -1 || t[y].dis == -1) continue;
int xrt = Getrt(x);int yrt = Getrt(y);int tmp = 0;
if(xrt == yrt) continue;
tmp = Merge(xrt,yrt);
t[xrt].fa = t[yrt].fa = tmp;
}
else
{
scanf("%d",&x);
if(t[x].dis == -1)
{
printf("-1\n");
continue;
}
int xrt = Getrt(x);int tmp = 0;
printf("%d\n",t[xrt].val);
tmp = Merge(t[xrt].lc,t[xrt].rc);
t[t[xrt].rc].fa = t[t[xrt].lc].fa = tmp;
t[xrt].dis = -1;t[xrt].fa = tmp;t[xrt].lc = t[xrt].rc = 0;
}
}
return 0;
}
P2713 罗马游戏
P2713 罗马游戏
模板题
P1456 Monkey King
P1456 Monkey King
猴子们结识朋友的方式就是并查集的合并方式,题目中求每一次决斗后,两群猴子中最强壮猴子的强壮值。
考虑到既要具备快捷合并的性质,又要具备快捷求最值的性质,左偏树是一个不错的选择,那么直接套用左偏树就好了。
决斗的时候,将两群猴子中的猴王(堆顶)拆出来,对其强壮值进行操作,然后再将两群猴子合并,输出新猴王(堆顶结点)的强壮值即可。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,x,y,rt1,rt2,rt3,rt4;
struct node{
int fa;
int dis;int val;
int lc;int rc;
}t[100015];
int Getrt(int x)
{
if(t[x].fa == x) return x;
else
{
t[x].fa = Getrt(t[x].fa);
return t[x].fa;
}
}
int Merge(int x,int y)
{
if(!x || !y) return x + y;
if(t[x].val < t[y].val) swap(x,y);
t[x].rc = Merge(t[x].rc,y);
t[x].fa = t[t[x].lc].fa = t[t[x].rc].fa = x;
if(t[t[x].lc].dis < t[t[x].rc].dis) swap(t[x].lc,t[x].rc);
t[x].dis = t[t[x].rc].dis + 1;
return x;
}
int main()
{
while(cin >> n)
{
for(int i = 0;i <= 100010;i ++)
{
t[i].val = -1;t[i].fa = 0;
t[i].lc = t[i].rc = 0;t[i].dis = -1;
}
for(int i = 1;i <= n;i ++)
{
scanf("%d",&t[i].val);
t[i].fa = i;t[i].dis = 0;
}
scanf("%d",&m);
for(int i = 1;i <= m;i ++)
{
scanf("%d%d",&x,&y);
x = Getrt(x);y = Getrt(y);
if(x == y) printf("-1\n");
else
{
t[t[x].lc].fa = t[x].lc;t[t[x].rc].fa = t[x].rc;
rt1 = Merge(t[x].lc,t[x].rc);
t[t[y].lc].fa = t[y].lc;t[t[y].rc].fa = t[y].rc;
rt2 = Merge(t[y].lc,t[y].rc);
t[x].lc = t[x].rc = t[y].lc = t[y].rc = 0;
t[x].val >>= 1;t[y].val >>= 1;t[x].dis = t[y].dis = 0;
t[x].fa = x;t[y].fa = y;
rt3 = Merge(rt1,rt2);
rt4 = Merge(x,y);
rt4 = Merge(rt3,rt4);
printf("%d\n",t[rt4].val);
}
}
}
return 0;
}
P1552 [APIO2012]派遣
P1552 [APIO2012]派遣
简化题意后,我们知道:
有一棵树,从中选一结点
a
a
a,在
a
a
a 为树根的子树中选一些点
v
v
v 组成点集
S
S
S,使得
ans
=
max
{
∣
S
∣
×
l
(
a
)
}
,
∑
v
∈
S
c
o
s
t
v
⩽
M
\text{ans}=\max\{|S|\times l(a)\},\sum_{v\in S}cost_v \leqslant M
ans=max{∣S∣×l(a)},v∈S∑costv⩽M
由于
l
(
a
)
l(a)
l(a) 是个定值,所以选定
a
a
a 后,只管令
∣
S
∣
|S|
∣S∣ 尽可能大。所以由贪心的思想想到,在子树中选的点的花费都要尽可能地小,才能使
∣
S
∣
|S|
∣S∣ 尽可能的大。从叶子结点开始枚举
a
a
a ,向上层层合并,选取所有情况中的满意度最大值。
既要具备合并的性质,又要具备数据结构内数据有序的性质,我们可以考虑用左偏树。
将这些结点以大根堆的形式储存,每到一个结点
a
a
a :
- 将以它为根的子树按左偏树的结构重新构造,然后从新的根 a ′ a' a′ 开始
- 如果 ∑ v ∈ S c o s t v > M \sum_{v\in S}cost_v > M ∑v∈Scostv>M,那么由于这个根结点的花费最大,所以去掉这个根结点,合并左右子树,得到新的根 a ′ ′ a'' a′′
- 然后重复以上步骤,直至 ∑ v ∈ S c o s t v ⩽ M \sum_{v\in S}cost_v \leqslant M ∑v∈Scostv⩽M 时,此时树中留下的结点都是花费最小的结点,所以此时 ∣ S ∣ |S| ∣S∣ 最大
- 计算 ∣ S ∣ max × l ( a ) |S|_{\max}\times l(a) ∣S∣max×l(a),并以它更新答案
在此过程中,只要在每个结点处再记录其子树的总花费和子树的大小,并递归维护,那么就能快速得到这两个值,从而保证程序的执行效率。
此外,我们还发现,删掉的结点没有重新加入左偏树的必要,因为它们的加入必定使总花费大于
M
M
M ,而且随着向上递归,可能有花费更小的结点加入到左偏树中而不会使总花费大于
M
M
M ,所以我们全程只需要维护一棵左偏树,并且根据约束条件删去或保留结点即可。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
int n,u,v,ind[100055],bos[100055];/*给定的上级关系*/
ll ans,m,lea[100055];
bool vis[100055];
struct ltnode{
ll cost, sumc, siz;
int dis;
int lson, rson;
int fa/*在左偏树中的父亲结点*/;
}a[100055];
int Getfa(int x)
{
if(a[x].fa == x) return x;
else
{
a[x].fa = Getfa(a[x].fa);
return a[x].fa;
}
}
int Merge(int x,int y)
{
if(!x || !y) return x + y;
if(a[x].cost < a[y].cost) swap(x,y);
a[x].rson = Merge(a[x].rson,y);
a[a[x].lson].fa = a[a[x].rson].fa = x;
if(a[a[x].lson].dis < a[a[x].rson].dis) swap(a[x].lson,a[x].rson);
a[x].dis = a[a[x].rson].dis + 1;
a[x].sumc = a[x].cost;a[x].siz = 1;
if(a[x].lson > 0) a[x].sumc += a[a[x].lson].sumc,a[x].siz += a[a[x].lson].siz;
if(a[x].rson > 0) a[x].sumc += a[a[x].rson].sumc,a[x].siz += a[a[x].rson].siz;
return x;
}
void up(int x)
{
int rt = Getfa(x);
int trt = 0;
while(a[rt].sumc > m)//超过M
{
trt = Merge(a[rt].lson,a[rt].rson);//将最大花费的结点删去
a[a[rt].lson].fa = a[a[rt].rson].fa = trt;
a[rt].fa = trt;a[rt].lson = a[rt].rson = 0;a[rt].dis = -1;
rt = trt;
}
ans = max(ans,a[rt].siz*lea[x]);
rt = Getfa(x);trt = Getfa(bos[x]);
rt = Merge(rt,trt);
a[rt].fa = a[trt].fa = rt;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 0;i <= n+5;i ++) a[i].dis = -1;
for(int i = 1;i <= n;i ++)
{
scanf("%d%lld%lld",&bos[i],&a[i].cost,&lea[i]);//录入初始数据
a[i].dis = 0;a[i].lson = a[i].rson = 0;a[i].fa = i;
ind[bos[i]] ++;//
a[i].sumc = a[i].cost;a[i].siz = 1;
}
for(int i = n;i >= 1;i --)//从叶子结点一直合并到根结点
up(i);//枚举所有结点
printf("%lld",ans);
return 0;
}
未完待续…
参考资料:LJZ. 很闲的左偏树和二项堆乱讲[PDF]