Leftist Tree(左偏树)学习笔记
基本概念
左偏树。顾名思义,就是向左偏的二叉树。它满足堆性质与左偏性质。
外结点与距离
我们定义不足两个儿子的节点为外结点。\(dist_x\) 称为 \(x\) 的距离,且有 \(dist_x = \min\{ dist _ {lc}, dist _ {rc}\}+1\)。这是递归定义,其实 \(dist _ x\) 就是 \(x\) 的子树中与 \(x\) 距离最近的外结点与 \(x\) 的距离。特殊的,定义空节点的距离为 \(-1\)。
堆性质与左偏性质
众所周知,堆性质就是 \(\forall x|val _ x \le val _ {lc}\ ,val _ x \le val _ {rc}\)。
而左偏性质就是 \(\forall x|dist _ {lc} \ge dist _ {rc}\)。
基本结论
-
\(\forall x|dist _ x = dist _ {rc} + 1\)。
-
距离为 \(n\) 的左偏树至少有 \(2 ^ {n+1} - 1\) 个结点,并且是一颗满二叉树。
-
根节点的距离 \(dist _ {root} \le \log{n}\)。
基本操作
合并 merge(x,y)
即合并以 \(x ,y\) 为根的两棵左偏树,并返回合并后根节点的编号。
这是左偏树最基本的操作。
在合并时我们需要保持堆性质与左偏性质。
首先,merge(x,y) 操作默认令 \(x\) 为合并后的根节点(当 \(x ,y\) 皆不为空节点时)。
为了保持堆性质,我们就得在 \(val _ x > val _ y\) 时将 \(x ,y\) 交换,以此来保持堆性质。
由于左偏性质的存在,将 \(y\) 与 \(x\) 的右儿子合并比将 \(y\) 与 \(x\) 的左儿子合并是更优的,这样可以使得合并后的树的深度更小,也就使每次操作的时间复杂度稳定在 \(\log{n}\)。
所以只需要递归合并 \(x\) 的右儿子与 \(y\) 即可。
但是这就导致了一个问题:\(x\) 的右儿子与 \(y\) 合并后可能导致在 \(x\) 的儿子节点中出现 \(dist _ {lc} < dist _ {rc}\) 的情况,这就会违背左偏性质,此时就需要将 \(lc , rc\) 交换。
最后维护一下 \(dist _ x\) 即可(\({dist} _ x = {dist} _ {lc} +1\))。
一般来说,merge(x,y) 后的根节点是 \(x\),但当 \(x ,y\) 中有一个是空节点时应返回不是空节点的那个。
int merge(int x,int y){
if(!x||!y) return x+y;//就是将 if(!x) return y;if(!y) return x; 简写了一下
if(t[x].val>t[y].val) swap(x,y);//维护堆性质,也可以将“>”换成“<”以此来换成大根堆
t[x].ch[1]=merge(t[x].ch[1],y);//依照左偏性质,与右儿子合并更优
if(t[t[x].ch[0]].dis<t[t[x].ch[1]].dis) swap(t[x].ch[0],t[x].ch[1]);//维护左偏性质
t[x].dis=t[t[x].ch[1]].dis+1;//更新x的距离
return x;
}
在这里提一嘴,大多数左偏树的题目都需要查找根节点,此时我们就只需要多维护一个并查集数组 \(rt _ x\) 即可。
在合并时也应同步更新 \(rt\) 数组,显然可以直接在维护完一系列性质之后用 t[ls].rt=t[rs].rt=t[x].rt=x; 更新并查集数组(在维护距离前后),这样在每一次合并后这个左偏树上每一个节点的 \(rt\) 都会被重置,重新变成一个多层的树,并且 merge(x,y) 合并后需要令 t[x].rt=t[y].rt=merge(x,y);。
但这样一来就可以查询任意一个节点所在的左偏树的根。
其他基本操作
插入一个数
只需要新建一个权值等于插入树的节点,将它与左偏树合并即可,时间复杂度 \(\mathcal{O}(\log _ 2 {n})\)。
求一个给定节点所在左偏树的根节点
前面已经提到了维护一个并查集数组 \(rt\),所以我们可以用一个 fnd(x) 函数来查询,和并查集是一样的,为了保证时间复杂度,我们需要用到路径压缩。
int fnd(int x){
return ((t[x].rt==x)?x:t[x].rt=fnd(t[x].rt));//路径压缩并查集
}
求最小/最大值
根据左偏树的堆性质,左偏树上根节点的值就是最小/最大值。
删除一个点
只要合并它的左右儿子,再删除这个节点的信息即可。
如果维护了并查集数组就得在删除节点后进行更新,令 t[ls].rt=ls;t[rs].rt=rs;t[x].rt=merge(ls,rs); 即可。
所以被删除的节点就不能再用了,如果要还原就得新建一个节点。
几道例题
P3377 【模板】左偏树(可并堆)
作为一道模板题,此题的题面还是很简洁的,总的来说只要按照题面上所说的步骤模拟即可。
Code:
#include<iostream>
using namespace std;
const int N(1e5+5);
int n,m;
bool ok[N];
namespace leftist_tree{
int ch[N][2],val[N],rt[N],dis[N];
inline int merge(int x,int y){
if(!x||!y) return x+y;
if(val[x]>val[y]||(val[x]==val[y]&&x>y)) swap(x,y);
ch[x][1]=merge(ch[x][1],y);
if(dis[ch[x][0]]<dis[ch[x][1]]) swap(ch[x][0],ch[x][1]);
rt[ch[x][0]]=rt[ch[x][1]]=rt[x]=x;
dis[x]=dis[ch[x][1]]+1;
return x;
}
inline int fnd(int x){
return ((rt[x]==x)?x:rt[x]=fnd(rt[x]));
}
inline void pop(int x){
val[x]=-1;
rt[ch[x][0]]=ch[x][0];rt[ch[x][1]]=ch[x][1];
rt[x]=merge(ch[x][0],ch[x][1]);
}
}
using namespace leftist_tree;
int main(){
#ifdef ytxy
freopen("in.txt","r",stdin);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
dis[0]=-1;
for(int i=1;i<=n;i++)
cin>>val[i],rt[i]=i;
while(m--){
int op,x,y;
cin>>op>>x;
if(op==1){
cin>>y;
if(val[x]==-1||val[y]==-1) continue;
int fx=fnd(x),fy=fnd(y);
if(fx!=fy) rt[fx]=rt[fy]=merge(fx,fy);
}
else{
if(val[x]==-1) cout<<-1<<'\n';
else cout<<val[fnd(x)]<<'\n',pop(fnd(x));
}
}
}
P1456 Monkey King
也是一道左偏树的模板题,初步读题可以知道这一题其实就是要求我们编写一个数据结构,资瓷取出最大值、加入一个数、合并两个数值所在的数据结构。
很明显可以用左偏树来写。
仍然是模拟题意即可。
Code:
#include<iostream>
using namespace std;
const int N(1e5+5);
int n,m;
namespace Leftist_Tree{
struct {
int ch[2],val,rt,dis;
} t[N];
#define ls t[x].ch[0]
#define rs t[x].ch[1]
int fnd(int x){
return ((t[x].rt==x)?x:t[x].rt=fnd(t[x].rt));
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(t[x].val<t[y].val) swap(x,y);
rs=merge(rs,y);
if(t[ls].dis<t[rs].dis) swap(ls,rs);
t[ls].rt=t[rs].rt=t[x].rt=x;
t[x].dis=t[rs].dis+1;
return x;
}
}
using namespace Leftist_Tree;
void fight(int x){
t[x].val>>=1;
int root;
root=merge(ls,rs);
ls=rs=t[x].dis=0;
t[root].rt=t[x].rt=merge(x,root);
}
int main(){
#ifdef ytxy
freopen("in.txt","r",stdin);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
while(cin>>n){
t[0].dis=-1;
for(int i=1;i<=n;i++)
cin>>t[i].val,t[i].rt=i,t[i].ch[0]=t[i].ch[1]=0;
cin>>m;
while(m--){
int x,y;
cin>>x>>y;
int fx=fnd(x),fy=fnd(y);
if(fx==fy){
cout<<-1<<'\n';
continue;
}
fight(fx);
fight(fy);
t[fx].rt=t[fy].rt=merge(t[fx].rt,t[fy].rt);
cout<<t[t[fx].rt].val<<'\n';
}
}
}

浙公网安备 33010602011771号