二叉堆++

68

我们知道,二叉堆有一些很好的性质,比如可以 \Theta(1) 获取最值,\Theta(n) 建树,\Theta(logn) 维护。但是在一些更复杂的操作的时候,总是显得有些力不从心。
比如这道 Monkey King,猴子每次打架的时候需要对两个大根堆进行合并。而使用普通的方法,每次合并的时候复杂度的量级为线性,容易超时。因此就需要特殊性质的堆来解决。典型的有配对堆和左偏树。

左偏树

定义

以下来自 OI Wiki

左偏树是一种 可并堆,具有堆的性质,并且可以快速合并。
外节点 为子节点数小于两个的节点,定义一个节点的 dist 为其到子树中最近的外节点所经过的边的数量。空节点的 dist 为 0。
左偏树是一棵二叉树,它不仅具有堆的性质,并且是「左偏」的:每个节点左儿子的 dist 都大于等于右儿子的 dist

因此,左偏树每个节点的 dist 都等于其右儿子的 dist 加一。

操作

  1. 合并
    见下,由于根的 dist 不会超过 \lfloor log_2(n+1) \rfloor (因为二叉树的前 n-1 是满二叉树),而递归每次使 dist 减一,因此合并的复杂度为 log_2m+log_2n
  2. 插入
    等同于合并一个节点和堆
  3. 删除根
    合并根的左右节点
  4. 删除任意节点
    合并左右儿子,向上更新 dist,交换左右儿子以满足性质。复杂度 \Theta(logn)

细节

由于左偏树的性质,有可能退化为链,这种情况下在左偏树中寻找根节点有点困难(可能会被卡),要配合并查集一起食用

模板

#define ch(x, side) (tr[x].ch[side])
struct NODE{
    int fa,ch[2],v,dist;
} tr[N];
int &rson(int x){
    return tr[tr].dist>tr[rs(x)].dist?rs(x):ls(x);
}
int find(int x){
    return tr[x].fa==x?x:(tr[x].fa=find(tr[x].fa));
}

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);
    // 下面是静态右儿子的做法
    // tr[rs(x)=merge(rs(x),y)].fa=x;
    // if(dist(ls(x))<dist(rs(x))){
    //     swap(ls(x),rs(x));
    // }
    // dist(x)=dist(rs(x))+1;
    // return x;
    int& rs_ref = rson(x);
    tr[rs_ref = merge(rs_ref, y)].fa = x;
    dist(x) = dist(rson(x)) + 1;
    return x;
}
void pushup(int x){
    if(!x) return;
    if(dist(x)!=dist(rson(x))+1){
        dist(x)=dist(rson(x))+1;
        pushup(tr[x].fa);
    }
}
void delete(int x){
    int y=merge(ch(x,0), ch(x,1));
    tr[y].fa=tr[x].fa;
    ch(tr[x].fa, ch(tr[x].fa,1)==x)=y;
    pushup(tr[x].fa);
}

碎碎念

被这道题卡了好久

最后发现,我根本不会并查集
为什么这么说呢?大家可能都清楚并查集的用处,路径压缩,启发式合并,菊花图,但是事实上并查集有很多细节
比如说这道题,带删除的,就会使并查集的路径压缩失效。那怎么处理呢?就是把被删除的节点的父亲指向自己的儿子,也就是回溯

这道题还有很多细节值得推敲,比如把dist置0作判断是否存在的条件,merge前先检查
本来写的是动态计算右儿子的做法,但是最后觉得不好调试就改了现在改了