左偏树学习笔记
一、前言
左偏树是一种可以在
具体来说,
插入一个元素:
查询最值:
删除最值:
合并:
减少一个元素的值:
同时它可以持久化。
二、定义
- 外节点:左儿子或者右儿子为空的节点。
- 一个节点
的距离 代表节点 到其所在的子树内的最近的外节点的距离。特别地,如果 是外节点, ;如果 是空节点, 。
三、性质
- 左偏树满足小/大根堆的性质,即对于所有节点
,满足 且 ,或者对于所有节点 ,满足 且 。 - 左偏树具有左偏性质,即对于任何一个节点
,有 。
四、结论
。这是因为根据定义, 并且 。- 距离为
的左偏树一共至少 个节点,取等时它是满二叉树。这是因为根据定义,对于左偏树中的每一个节点 都满足 ,否则 ,与性质矛盾。所以左子树的第 层的所有节点全部存在。递归地考虑,知道它至少是一棵满二叉树。所以至少 个节点。 - 一棵有
个节点的左偏树距离至多为 。可以由结论 2 推得。
五、实现
我们以根节点的编号指代一棵左偏树。这里我们假定维护的是小根堆,即全部求的是节点权值的最小值。
merge(x,y)
:合并两个以节点 、 为根的左偏树,返回合并后的左偏树的根节点编号。这是左偏树最基础的操作。- 首先,如果两棵树中有一棵为空,那么返回另外一棵树。
- 如果两棵树都非空,不妨假设
小于 ,否则交换 , 以避免讨论。 - 将
与 的右子树 合并。 - 返回时,如果回溯到节点
时, 的右子节点 的距离 大于 的左子节点 的距离。 ,那么这不满足左偏树的左偏性质。所以我们交换 的左右子树即可。 - 最后,由于节点
的距离可能发生改变,我们要更新 的距离,即令 。
总结:这样就可以合并两棵左偏树了。由于所有遍历过的节点都被更新了,所以这种修改后的依然是一棵左偏树。时间复杂度是原先的 ,是 级别的。
- 求最小值:根节点的值,时间复杂度
。 - 删除最小值:删除根节点,合并根节点的左右子树,时间复杂度与合并两棵左偏树相同,为
。 - 插入新节点:容易知道一个点也是一棵左偏树,所以可以看做两棵左偏树的合并。时间复杂度也是
。 - 左偏树的建树:将所有节点作为左偏树放进先进先出的队列里,每次拿出队首的两棵左偏树,将它们合并后放到队尾。那么前
次合并的是距离为 的左偏树,然后 次合并的是距离为 的左偏树,……,然后 次合并的是距离为 的左偏树。总复杂度为 。 - 在左偏树上删除一个指定节点(并非删除值为
的节点,而是删除编号为 的节点)。- 首先,我们将
的左子树、右子树合并,得到 的子树新的根节点 。 - 如果
是全局的根节点,结束操作。 - 否则继续分类讨论,令
为 的父亲节点。- 新树的距离为
,如果满足 ,那么结束操作。 - 如果满足
,那么 的距离应该更新为 ,而且如果 是 的左子节点,要交换 的左右子树。然后继续向上更新 的父亲节点。 - 如果满足
,那么 的距离应该更新为 ,而且如果 是 的右子节点,要交换 的左右子树。然后继续向上更新 的父亲节点。
总结:这样就可以删除任何一个指定节点了。合并是 的,向上更新是 的,总复杂度 。
- 新树的距离为
- 首先,我们将
- 获得节点
所在的左偏树的根节点。直接向上跳,使用路径压缩即可。需要维护 的值,在合并和删除时要更新。这个操作最坏 。
合并:
点击查看代码
int merge(int x,int y){
if(!x||!y)return x+y;
if(val[x]<val[y])swap(x,y);
r[x]=merge(r[x],y);
if(dis[l[x]]<dis[r[x]])swap(l[x],r[x]);
dis[x]=dis[r[x]]+1;
return x;
}
删除非根节点:
点击查看代码
void pushup(ll x){
if(!x)return;
if(dis[x]!=dis[r[x]]+1){
dis[x]=dis[r[x]]+1;
pushup(fa[x]);
}
}
ll merge(ll x,ll y){
if(!x||!y)return x+y;
if(val[x]<val[y])swap(x,y);
fa[r[x]=merge(r[x],y)]=x;
if(dis[l[x]]<dis[r[x]])swap(l[x],r[x]);
pushup(x);
return x;
}
六、例题
例 1.P3377 【模板】左偏树(可并堆)
点击查看代码
int n,m,v[100010],l[100010],r[100010],d[100010],dl[100010],rt[100010];
int merge(int x,int y){
if(!x||!y)return x+y;
if(v[x]>v[y]||x>y&&v[x]==v[y])swap(x,y);
r[x]=merge(r[x],y);
if(d[r[x]]>d[l[x]])swap(l[x],r[x]);
d[x]=d[r[x]]+1;
return x;
}
int find(int x){
return x==rt[x]?x:rt[x]=find(rt[x]);
}
int main(){
n=read();m=read();d[0]=-1;
for(int i=1;i<=n;i++)v[i]=read(),rt[i]=i;
for(int op,x,y;m;m--){
op=read();x=read();
if(op==1){
y=read();
if(dl[x]||dl[y])continue;
x=find(x);y=find(y);
if(x!=y)rt[x]=rt[y]=merge(x,y);
}else{
if(dl[x]){puts("-1");continue;}
x=find(x);
write(v[x]);putchar(10);
rt[x]=rt[l[x]]=rt[r[x]]=merge(l[x],r[x]);
dl[x]=1;d[x]=l[x]=r[x]=0;
}
}
return 0;
}
例 2.P1456 Monkey King
点击查看代码
int merge(int x,int y){
if(!x||!y)return x+y;
if(val[x]<val[y])swap(x,y);
r[x]=merge(r[x],y);
if(dis[l[x]]<dis[r[x]])swap(l[x],r[x]);
dis[x]=dis[r[x]]+1;
return x;
}
int find(int x){
return x==rt[x]?x:rt[x]=find(rt[x]);
}
int work(int x){
val[x]/=2;
int p=rt[l[x]]=rt[r[x]]=merge(l[x],r[x]);
l[x]=r[x]=dis[x]=0;
return rt[p]=rt[x]=merge(p,x);
}
int main(){
while(cin>>n){
for(int i=1;i<=n;i++){
val[i]=read();
rt[i]=i;
l[i]=dis[i]=r[i]=0;
}
m=read();
for(int x,y,p;m;m--){
x=find(read());y=find(read());
if(x==y)puts("-1");
else{
x=work(x);y=work(y);
rt[x]=rt[y]=merge(rt[x],rt[y]);
write(val[rt[x]]);
putchar(10);
}
}
}
return 0;
}
例 3.P2713 罗马游戏
点击查看代码
int merge(int x,int y){
if(!x||!y)return x+y;
if(val[x]>val[y])swap(x,y);
r[x]=merge(r[x],y);
if(dis[l[x]]<dis[r[x]])swap(l[x],r[x]);
dis[x]=dis[r[x]]+1;
return x;
}
int find(int x){
return x==rt[x]?x:rt[x]=find(rt[x]);
}
int main(){
n=read();
for(int i=1;i<=n;i++)val[rt[i]=i]=read();
m=read();
for(int i=1,x,y;i<=m;i++){
char op;
cin>>op;
if(op=='M'){
x=read();y=read();
if(del[x]||del[y])continue;
x=find(x);y=find(y);
if(x!=y)rt[x]=rt[y]=merge(x,y);
}else{
x=read();
if(del[x])putchar(48);
else{
x=find(x);
write(val[x]);
rt[x]=rt[l[x]]=rt[r[x]]=merge(l[x],r[x]);
l[x]=r[x]=dis[x]=val[x]=0;
del[x]=1;
}
putchar(10);
}
}
return 0;
}
例 4.P1552 [APIO2012] 派遣
点击查看代码
int n,m,v[100010],b[100010],l[100010],r[100010],d[100010];
int sz[100010],rt[100010],c[100010],tag=0;
ll ans=0,sum[100010];
int merge(int x,int y){
if(!x||!y)return x+y;
if(c[x]<c[y])swap(x,y);
r[x]=merge(r[x],y);
if(d[r[x]]>d[l[x]])swap(l[x],r[x]);
d[x]=d[r[x]]+1;
return x;
}
int main(){
n=read();m=read();
for(int i=1;i<=n;i++){
b[i]=read();c[i]=read();v[i]=read();
rt[i]=i;sz[i]=1;sum[i]=c[i];
}
for(int i=n;i>=1;i--){
while(sum[i]>m){
sum[i]-=c[rt[i]];sz[i]--;
rt[i]=merge(l[rt[i]],r[rt[i]]);
}
ans=max(ans,1ll*v[i]*sz[i]);
sum[b[i]]+=sum[i];
sz[b[i]]+=sz[i];
rt[b[i]]=merge(rt[b[i]],rt[i]);
}
cout<<ans<<'\n';
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!