#edu3011. 【教程】可并堆-左偏树
【教程】可并堆-左偏树
普通堆能够动态维护一个集合最值,以大根堆为例,支持 插入一个数、删除堆顶, 获得堆顶(最值)。但不方便合并两个堆,尽管可以利用启发式合并,但启发式合并本质上还是暴力合并。为了更加高效合并堆,前人发明了很多可并堆,例如左偏树、配对堆、随机堆、斜堆、二项堆、斐波那契堆,左偏树常数小、代码容易编写,这里重点介绍左偏树。pb_ds 中提供了配对堆,感兴趣可以拓展学习。
为什么需要可并堆?
尽管平衡树能够动态维护集合最值,并且还支持高效删除、修改任意节点,但是平衡树无法高效合并两个集合,这就是可并堆不可被取代的原因。
支持将两个堆高效合并,就是可并堆。左偏树能够支持传统堆的所有操作,还可以删除或修改堆中任意节点(还支持可持久化),最关键的是能够支持高效合并。当然左偏树,常数小、代码容易编写,这对于竞赛选手来说非常友好。
什么是左偏树?
要定义左偏树,需要明确两个基本概念:外节点、到外节点距离 dis。
- 外节点:在二叉树上,儿子个数小于 的节点。
- 到外节点距离:树上任意一个节点到最近外节点经过的边的条数。特殊地,外节点自身到外节点距离为 , 空节点到外节点距离为 。(部分资料空节点距离为 ,所有节点距离会增大 )
左偏树,是一棵二叉树,首先满足堆的性质,并且满足左偏性质。左偏性质:每个节点左节点 dis 大于等于 右儿子 dis。构造的左偏性质是为了高效合并。
左偏树(圈外数字是dis)
左偏树重要性质
为了方便描述,明确一下数组含义。
根据需要左偏树每个节点需要维护:左右儿子 ls[x],rs[x]、节点权值 val[x]、 距离 dis[x]。
由于经常需要尽快到达左偏树根,需要利用并查集快速找根。因此需要维护并查集代表 fa[x]。
如果需要删除任意节点(修改任意节点),还需要维护每个节点在左偏树上的亲父亲节点,记为 up[x]。
-
性质1:满足堆的性质,以小根堆为例,每个节点权值都满足堆的性质,即 。左右儿子之间没有大小关系。
-
性质2:左偏性质,左儿子距离大于等于右儿子,即 。
-
性质3:任意节点距离等于右儿子距离加 ,即 。特殊规定 ,这样之后不需要特殊判断,直接可以直接等于右儿子距离加 .
-
性质4: 个节点的左偏树,树根的 最大,满足 .
- 证明:根的距离为 的左偏树,那么二叉树上有 层是满的,至少有 个节点。即 ,得到 .
-
性质5:每个节点 ,代表节点子树内满的层数,数量级上小于 。这条性质主要是为了说明删除一个节点,向上调整 dis 的复杂度。
核心操作:合并
左偏树核心操作就是合并,合并时要同时兼顾符合堆和左偏的性质。以合并小根堆为例(本文讨论都以小根堆为例),合并时选择权值小的节点作为整个合并后的根。然后递归合并其右儿子与另外一个堆,作为合并后根的右儿子。
合并之后,要维护左偏性质,如果右儿子dis 大于左儿子dis ,那么就交换两个儿子。
核心代码如下:
dis[0]=-1 ; //规定空节点 dis 为 -1 ,后面就不需要特判了
int merge(int x,int y)
{
if(x==0||y==0)return x+y;
//小根堆
if(val[x]>val[y])swap(x,y); //x 要做堆顶
rs[x]=merge(rs[x],y); //递归合并根的右儿子与另外一个堆
//回溯 保证左偏性质
if(dis[ls[x]]<dis[rs[x]])swap(ls[x],rs[x]);
//更新根的距离
dis[x]=dis[rs[x]]+1;
//返回合并后的根
return x;
}
递归合并过程,可以用下面两个左偏树实例如下:
左偏树合并过程举例
合并时间复杂度分析
递归合并,每递归一层,其中一个堆的 dis 就减少 1,最终合并到两个堆空节点上,总的复杂度就是两个堆根 dis 之和,即 ,如下图:
左偏树合并过程举例
模板题
1.P3377 【模板】可并堆 1
【题意简化】一开始有 个小根堆,每个堆包含且仅包含一个数。 次操作:
1 x y: 将第 x 个数和第 y 个数所在的小根堆合并2 x: 输出第 x 个数所在的堆最小数,并将这个最小数删除
【分析】比较常规的可并堆操作,只涉及到堆合并、求堆顶、删除堆顶。
使用左偏树实现时,需要特别注意,由于左偏树深度量级为 ,不能直接一级一级往上爬暴力找堆的根,要结合并查集,快速跳转到堆的根节点。
删除堆顶,只需要合并堆的根节点 的左右儿子,将左右儿子变为堆的新的根。需要特别注意,旧根 不能直接删掉,要修改其 fa[x] 数组,让 fa[x] 等于新根。因为在并查集上,还有其他节点指向 ,这样操作 就变成一个中转节点。
删除堆顶,核心参考代码:
void pop(int x) //删除 x 所在的堆顶
{
if(del[x]){
cout<<"-1\n";continue; // 已经被删除了
}
x=find(x); //快速跳到堆根节点
cout<<v[x]<<"\n";
del[x]=true;
//注意 x 的 fa[] 要修改为新根
fa[ls[x]]=fa[rs[x]]=fa[x]=merge(ls[x],rs[x]);
}
完整参考代码:点击
其他类似模板题
- P2713 罗马游戏
- 只涉及到合并堆、求堆顶、删除堆顶
- P1456 Monkey King
- 虽然需要修改任意节点,但大根堆,节点权值只会改小。对于需要修改的节点,修改 以上的树还是满足左偏树, 下面的合并就是一个新堆,然后再次合并就可以了。即修改 ,之后不会往上修改。
2.P11266 【模板】可并堆 2
【题意简化】你需要维护序列 以及 个集合 ,初始时 。
接下来要进行以下四种操作共 次,每次操作形如:
0 x y:表示将元素 从集合 中删去。保证此时元素 在集合 中。1 x:表示询问 ,保证此时集合 非空。2 x y:将集合 中并入 并清空集合 。保证此时集合 均非空,且此次操作后不会再出现涉及集合 的操作。3 x y z:表示将 赋值为 。保证此时元素 在集合 中,且 。
【分析】题意有点绕,实际就是最开始给定 个堆,每个堆一开始只有 ,对应权值为 .
需要合并堆、查询堆中最小值、删除任意一个节点、修改任意一个节点。要维护小根堆,存在删除任意节点、修改任意节点。
可持久化可并堆
3.P2483 【模板】k 短路 / [SDOI2010] 魔法猪学院
其他题目
1.P1552 [APIO2012] 派遣
【题意简化】
【分析】
思路一:树上启发式合并
树上每个节点对应一个优先队列,树上启发式合并容器,容器内代表子树费用小的点集。合并后,如果容器内总和超过 , 循环去掉费用高的点,直到总和小于等于 . 时间复杂度 .
参考代码:点击
思路二:可并堆
每个子树对应一个可并堆,可并堆内维护费用低的点,当总和超了 m ,循环删除堆顶。dfs 的过程合并子树的堆,时间复杂度为 .
2.P4971 断罪者
3.P3261 [JLOI2015] 城池攻占
4.P3273 [SCOI2011] 棘手的操作
5.P4331 [BalticOI 2004] Sequence
6.P10759 [BalticOI 2024] Jobs
学习完毕
{{ select(1) }}
- YES
- NO