左偏树(可并堆)学习笔记
引入:堆是一种很常用的数据结构,可以在 $ O_{log(n)} $的复杂度内进行插入,以维护最大/最小值。但是,如果我们将两个堆合并,即使是启发式合并,时间复杂度也高达 $ O_{(size_a \times log(size_b))} $,而这是我们所不想看到的。那么,有没有一种更好的解决方案呢?
下面来引入一种新的数据结构——左偏树。顾名思义,这是一颗树,但是向左偏。首先定义一个 \(dis_u\),表示 \(u\) 到最近的非满节点(即没有左儿子或右儿子的节点)的距离。
而左偏的性质,若设左儿子右儿子分别为 \(ls\)、\(rs\),则在这棵树上,对于每个结点,都有 \(dis_{ls} \geq dis_{rs}\)。显然,当 \(dis\) 为定值时,如果令这棵子树结点最多,那么这棵子树是一棵满二叉树。所以,对于一个大小为 \(n\) 的子树 \(u\),有 \(2^{dis_u+1}-1 \ge n\)。
利用这一性质,我们可以进行合并操作。合并时,只需要维护堆性质和左偏树性质即可。
int fa[N], ls[N], rs[N], val[N], dis[N];
int find(int x){
if(fa[x] ^ x){
fa[x] = find(fa[x]);
}
return fa[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), fa[rs[x]] = x;
if(dis[ls[x]]<dis[rs[x]]) swap(ls[x], rs[x]);
dis[x] = dis[rs[x]]+1;
return x;
}
因为每次往下走一层,\(dis\) 会减少 \(1\),而根据性质,\(dis\) 不会超过 \(log(size+1)\),因此,最坏情况(每次都交换)的复杂度为 \(O_{(log(m) + log(n))}\),其中 \(m\)、\(n\)为合并的两棵子树的大小。
如何删除堆顶元素呢?只需要合并左右儿子。
void del(int x){
val[x] = -1 ,fa[ls[x]] = ls[x], fa[rs[x]] = rs[x];
fa[x] = merge(ls[x], rs[x]);
}