左偏树
定义:
左偏树是 一种具有左偏性质的堆有序二叉树 (这里要注意,堆有序二叉树和二叉堆并不是同一种东西,因此左偏树并不是二叉堆)。——可并堆之左偏树
每一个节点存储的信息包括左右子节点、关键值、父节点以及距离。
等等,节点的距离并不是节点子树的大小,也不是深度,
节点的距离 \(dist\) 的定义和性质
对于一棵二叉树,我们定义 外节点 为左儿子或右儿子为空的节点,定义一个外节点的 \(dist\) 为 0,
一个不是外节点的节点 \(dist\) 为其到子树中最近的外节点的距离加一。空节点的 \(dist\) 为 -1。一棵有 个节点的二叉树,根的 \(dist\) 不超过 \(「\log(n+1)」\),因为一棵根的 \(dist\) 为 \(x\) 的二叉树至少有 \(x-1\) 层是满二叉树,那么就至少有 \(2^x-1\) 个节点。
注意这个性质是所有二叉树都具有的,并不是左偏树所特有的。
—— OI Wiki
需要注意的是,\(dist\) 不是深度,左偏树的深度没有保证,一条向左的链也是左偏树。
所以说,左偏树既是一颗二叉树,又具有「左偏」的性质:这个节点的 左儿子的 \(dist\) 大于 右儿子的\(dist\)。
下图为一棵左偏树:
操作:
可合并堆的操作,那肯定就是‘合并’了,为了找到合并的两个点的根,要用到‘并查集’
for (i = 1; i <= n; ++i) f[i] = i;
int find(int x) {
return x == f[x] ? x : f[x] = find(f[x]);
}
合并merge
合并操作是可并堆最重要的操作,以小根堆为例,合并\(x\)与\(y\)。
首先,我们要满足小根堆的性质,取\(x\)和\(y\)中较小的一个作为根;
然后将 较小的节点的右儿子 与较大的节点合并。
if(val[x] > val[y]) Swap(x, y);
rc[x] = merge(rc[x], y);
至于为什么是右儿子 与 较大的节点合并,这棵树毕竟是 左偏 的,我们连到 右子树 上是为了保证它偏的不是太厉害,并且右子树毕竟‘浅’嘛。否则,小心成‘链’。
等我们合并完,回溯的过程中,我们还要判断它是否还保持「左偏」,并且update一下根的dist
if(dist[rc[x]] > dist[lc[x]]) Swap(lc[x], rc[x]);
dist[x] = dist[rc[x]] + 1;
最后,我们还需要update它们的父亲father
f[find(x)] = f[find(y)] = merge(find(x), find(y));
代码
【合并mergeのCode】
f[find(x)] = f[find(y)] = merge(find(x), find(y));
int merge(int x, int y){
if(!x || !y) return x + y;
if(val[x] > val[y]) Swap(x, y);
rc[x] = merge(rc[x], y);
if(dist[rc[x]] > dist[lc[x]]) Swap(lc[x], rc[x]);
dist[x] = dist[rc[x]] + 1;
return x;
}
删除Delete
-
删除根节点
我们将 根节点 的左右儿子 合并起来,自然 根节点 就被删除掉了,
最后记得更新 father 的值
【删除根节点のCode】
f[x] = f[t[x].ch[0]] = f[t[x].ch[1]] = merge(t[x].ch[0], t[x].ch[1]);
-
删除任意节点
这时候我们将 节点 的左右儿子 合并起来,然后自底向上更新 \(dist\) 、不满足左偏性质时交换左右儿子,当 \(dist\) 无需更新时结束递归:
代码
【删除节点のCode】
int merge(int x, int y){
if(!x || !y) return x + y;
if(val[x] > val[y]) swap(x, y);
rc[x] = merge(rc[x], y);
if(dist[rc[x]] > dist[lc[x]]) swap(lc[x], rc[x]);
pushup(x);
return x;
}
void pushup(int x) {
if (!x) return;
if (dist[x] != dist[rc[x]] + 1) {
dist[x] = dist[rc[x]] + 1;
pushup(fa[x]);
}
}
插入节点insert
单个节点也可以视为一个堆,合并即可。
代码
【合并の新Code】
左偏树还有一种无需交换左右儿子的写法:
将 较大的儿子视作左儿子, 较小的儿子视作右儿子:
int &rs(int x) {
return ch[x][dist[rc[x]] < dist[lc[x]]];
}
int merge(int x, int y) {
if (!x || !y) return x | y;
if (val[x] < val[y]) swap(x, y);
rs(x) = merge(rs(x), y);
dist[x] = dist[rc[x]] + 1;
return x;
}
整个堆加上/减去一个值、乘上一个正数
代码
点击查看代码
buhui例题
【P3377 左偏树(可并堆)】
【模板】左偏树(可并堆)
题目描述
如题,一开始有 \(n\) 个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:
1 x y
:将第 \(x\) 个数和第 \(y\) 个数所在的小根堆合并(若第 \(x\) 或第 \(y\) 个数已经被删除或第 \(x\) 和第 \(y\) 个数在用一个堆内,则无视此操作)。2 x
:输出第 \(x\) 个数所在的堆最小数,并将这个最小数删除>(若有多个最小数,优先删除先输入的;若第 \(x\) 个数已经被删除,则输出 \(-1\) 并无视删除操作)。输入格式
第一行包含两个正整数 \(n, m\),分别表示一开始小根堆的个数和接下来操作的个数。
第二行包含 \(n\) 个正整数,其中第 \(i\) 个正整数表示第 \(i\) 个小根堆初始时包含且仅包含的数。
接下来 \(m\) 行每行 \(2\) 个或 \(3\) 个正整数,表示一条操作,格式如下:
操作 \(1\):1 x y
操作 \(2\):2 x
输出格式
输出包含若干行整数,分别依次对应每一个操作 \(2\) 所得的结果。
【完整代码】
#include<iostream>
#define M 100005
using namespace std;
inline void swap(int &x,int &y){
int temp=x;x=y;y=temp;
}
int ch[M][2],fa[M],vis[M];
struct heap{
int val,dis;
}h[M<<1];
int marge(int x,int y){
if(!x||!y)return x+y;
if(h[x].val>h[y].val)swap(x,y);
ch[x][1]=marge(ch[x][1],y);
if(h[ch[x][1]].dis>h[ch[x][0]].dis)swap(ch[x][0],ch[x][1]);
h[x].dis=h[ch[x][1]].dis+1;
return x;
}
int find(int x){
return fa[x]==x?fa[x]:fa[x]=find(fa[x]);
}
int main(){
int n,m,opt,x,y;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&h[i].val);
fa[i]=i;
}
for(int i=1;i<=m;i++){
scanf("%d",&opt);
if(opt==1){
scanf("%d%d",&x,&y);
if(vis[x]||vis[y])continue;
x=find(x);
y=find(y);
if(x!=y)fa[x]=fa[y]=marge(x,y);
}else{
scanf("%d",&x);
if(vis[x])puts("-1");
else{
x=find(x);
printf("%d\n",h[x].val);
vis[x]=true;
fa[ch[x][1]]=fa[ch[x][0]]=fa[x]=marge(ch[x][0],ch[x][1]);
ch[x][0]=ch[x][1]=h[x].dis=0;
}
}
}
return 0;
}