跳转至

C16【模板】左偏树(可并堆)_哔哩哔哩_bilibili

左偏树(可并堆)

二叉堆

从二叉堆的结构说起,它是一棵二叉树,并且是完全二叉树,每个结点中存有一个元素(或者说,有个权值)。

堆性质:父亲的权值不小于儿子的权值(大根堆)

插入数:先插入在堆尾,然后向上调整(每次与父节点比较交换)

删除堆顶:先与堆尾交换,删除堆尾,将堆顶向下调整

左偏树(可并堆)

实现目标

实现一个可以合并的小根堆(即支持合并两个小根堆为一个的小根堆,动态维护)

定义

左偏树是一种具有**左偏性质**的**堆有序**的二叉树。注意,左偏树不是二叉堆。

左偏树的每一个节点存储的信息包括左右子节点l[x],r[x]、权值v[x]和距离dis[x]。

左儿子或右儿子为空的节点,称为外节点。**节点的距离**的定义:该节点到与其最近的外节点经过的边数。

我们规定,外节点的距离为0,空节点的距离为-1。

(批注:二叉堆是完全二叉树,而左偏树不是)如下图,都是左偏树

image.png

性质

性质1:堆的性质,对于任意节点\(v[x] \leq v[lc[x]],v[rc[x]]\)(小根堆)。 性质2:左偏性质,左儿子的距离\(\geq \(右儿子的距离,\)dis[lc] \geq dis[rc]\)。 性质3:任意节点的距离=其右儿子的距离+1,\(dis[x]=dis[rc]+1\)。 根据性质3,我们可以知道为什么空节点的dis为-1了。 性质4:一棵有n个节点的二叉树,根的\(dis\leq log(n+1)-1\)

根据性质3,我们可以知道为什么空节点的dis为-1了

对于性质4,我们举例说明:

image.png

根的dis=2,说明至少有3层满二叉树。因为如果没有3层满二叉树,就比如上图中去掉点8或者点4,5,那么点3(或者点2)的dis就会是0,根的dis就是1.总结来说,就是对于有i层的满二叉树,才能保证在1~i-1层之内没有空节点

因为根的距离为x的二叉树至少有x+1层是满二叉树,那么至少有\(2^{x+1} - 1\)个节点。即n≥\(2^{x+1} - 1\),所以\(x≤\log(n+1)-1\)

合并操作

合并操作是左偏树最重要的操作: 1.维护堆的性质,先取值较小的那个根作为合并后的堆的根节点,然后递归合并其右儿子与另一个堆,作为合并后的堆的右儿子。 2.维护左偏性质,合并后若左儿子的dis小于右儿子的dis,就交换两个儿子。

在递推过程中,我们维护堆的性质。在回溯过程中,我们维护左偏性质(通过交换左右儿子)

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);//让x的v小于y,若相同,则让x的编号小于y.第二关键字由题目确定
    rc[x]=merge(rc[x],y);//递归合并

    if(dis[lc[x]]<dis[rc[x]])swap(lc[x],rc[x])//维护左偏性质
    dis[x]=dis[rc[x]]+1;//更新dis
    return x;//返回新的根节点

}

维护左偏性质:图示

image.png

v[x]==v[y]?x>y:v[x]>v[y]:图示

image.png

递归合并:图示

image.png

复杂度分析

image.png

每次递归都会让其中一个堆的根节点dis-1:图示

image.png

例题 #1

如题,一开始有 \(n\) 个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:

  1. 1 x y:将第 \(x\) 个数和第 \(y\) 个数所在的小根堆合并(若第 \(x\) 或第 \(y\) 个数已经被删除或第 \(x\) 和第 \(y\) 个数在用一个堆内,则无视此操作)。

  2. 2 x:输出第 \(x\) 个数所在的堆最小数,并将这个最小数删除(若有多个最小数,优先删除先输入的;若第 \(x\) 个数已经被删除,则输出 \(-1\) 并无视删除操作)。

提示

【数据规模】

对于 \(30\%\) 的数据:\(n\le 10\)\(m\le 10\)。 对于 \(70\%\) 的数据:\(n\le 10^3\)\(m\le 10^3\)。 对于 \(100\%\) 的数据:\(n\le 10^5\)\(m\le 10^5\),初始时小根堆中的所有数都在 int 范围内。

实现方法

由于数字的编号被保存在节点信息内,而非节点下标,所以为了找到所谓的“第x个数”,我们要么就枚举每一个点。这显然是不行的。

image.png

步骤如下

image.png

性质与区别

image.png

如下图,这是并查集的图像。可能在左偏树中,x是堆顶,其左右儿子分别为y和z,但是在并查集中,下一定也为根(我们这样维护),但y和z的位置就不一定了。

所以在合并后,我们不仅仅要将x的左右儿子y,z的fa指向新的根(新的根会在y,z之间产生),我们也要将x的fa指向新的根,因为在并查集中,会有其他点直接与x相连。即并查集**不是二叉树,x虽可抛,x的儿子不可抛**。在x在左偏树中被删除后,我们不在并查集中删除它,而是将其作为一个中继点,好让原来的x在并查集中的儿子们找到新的fa,进行路径压缩。

image.png

模板代码

/*////////ACACACACACACAC///////////
       . Coding by Ntsc .
       . ToFind Chargcy .
       . Prove Yourself .
/*////////ACACACACACACAC///////////

#include<bits/stdc++.h>
#define ll long long
#define db double
#define rtn return
#define i1n int i=1;i<=n;i++
#define in1 int i=n;i>=1;i--
using namespace std;

const int N=2e5+5;
const int M=1e5;
const int Mod=1e5;
const int INF=1e5;

int v[N],rc[N],fa[N],lc[N],n,m,op,x,y,dis[N];

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);//让x的v小于y,若相同,则让x的编号小于y.第二关键字由题目确定
    rc[x]=merge(rc[x],y);//递归合并

    if(dis[lc[x]]<dis[rc[x]])swap(lc[x],rc[x]);//维护左偏性质
    dis[x]=dis[rc[x]]+1;//更新dis
    return x;//返回新的根节点

}

int find(int x){//极简并查集
    return (x==fa[x])?x:fa[x]=find(fa[x]);
}

signed main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&v[i]);
        fa[i]=i;//并查集初始化
    }
    for(int i=1;i<=m;i++){
        scanf("%d%d",&op,&x);
        if(op==1){
            scanf("%d",&y);
            if(v[x]==-1||v[y]==-1)continue;//判断是否被删除
            x=find(x);y=find(y);//找到xy分别的并查集

            if(x!=y)fa[x]=fa[y]=merge(x,y);//判断是否在同一个堆中
        }else{
            if(v[x]==-1){
                printf("-1\n");
                continue;
            }x=find(x);
            printf("%d\n",v[x]);//x既是原x所在并查集的根,也是原来x所在的堆的根节点.既然是小根堆,那么根节点恰好是最小值.找到他即可
            v[x]=-1;//标记删除
            fa[lc[x]]=fa[rc[x]]=fa[x]=merge(lc[x],rc[x]);//合并x的两个儿子,抛弃x,并且返回新的根
        }
    }
    return 0;
}