Skip to content

Lec4 Heaps

Lec4: Heaps

https://oi-wiki.org/ds/heap/

左偏树

OI-wiki

性质

dist的定义

  • 如果一个结点的左孩子或右孩子为空结点,则该结点的 dist 为 0,这种结点被称为外结点
  • 如果一个结点的左孩子和右孩子都不为空,则该结点的 \(dist(x)=\min(dist(lson_x),dist(rson_x))+1\)

对于左偏树 \(\forall x,dist(lson_x)\geq dist(rson_x)\),因此

\(dist(x)=dist(rson_x)+1\)

dist不是深度(因为外节点可能在链的中间),左偏树的深度可能是O(n)的,一条向左的链也是左偏树。

操作

合并

将两个堆合并,但保持堆的大小关系 。合并是最关键的操作,因为插入=合并原树和只有一个点的树。删除根=合并左右儿子。

  1. 比较当前两个子树根节点的值,选择较小的为根(记为x), 另一个为y
  2. 递归合并x的右儿子与y
  3. 检查是否满足左偏性质,如果不满足,则交换子树。

感性理解,时间复杂度取决于根节点一直向右儿子走的路径("右侧路径")长度, 这个长度越短越好.

可视化

    int merge(int x,int y){
        if(!x||!y) return x+y;
        if(tree[x].val>tree[y].val) swap(x,y);//根是小的
        rson(x)=merge(rson(x),y);//合并右儿子
        if(tree[lson(x)].dist<tree[rson(x)].dist)
            swap(lson(x),rson(x));
        tree[x].dist=tree[rson(x)].dist+1;//更新dist
        return x;
    }
Code

题目

值得注意的是,左偏树的深度可能是O(n)的,只是dist的范围是O(logn) 所以为某一个节点找对应的根节点时,不能直接暴力向上跳,而是要用并查集维护每个节点根节点。

#include<bits/stdc++.h>
#define MAXN 100000
using namespace std;
struct Leftest{
private:
    struct node{
        int ls,rs;
        int val;
        int dist;
    }tree[MAXN+5];
    //一个并查集的根对应一棵左偏树的根
    int root[MAXN+5];
#define lson(x) (tree[x].ls)
#define rson(x) (tree[x].rs)
    int deleted[MAXN+5];
    int findrt(int x){
        if(root[x]==x) return x;
        else return root[x]=findrt(root[x]);
    }
    int merge(int x,int y){
        if(!x||!y) return x+y;
        if(tree[x].val>tree[y].val) swap(x,y);
        rson(x)=merge(rson(x),y);
        if(tree[lson(x)].dist<tree[rson(x)].dist)
            swap(lson(x),rson(x));
        tree[x].dist=tree[rson(x)].dist+1;
        return x;
    }
public:
    void merge2(int x,int y){
        if(deleted[x]||deleted[y]) return;
        int rx=findrt(x),ry=findrt(y);
        if(rx==ry) return;
        int now=merge(rx,ry);
        root[rx]=root[ry]=now;
    }
    int deleteMin(int x){
        if(deleted[x]) return -1;
        x=findrt(x);//找到x所在堆的根
        int val=tree[x].val;
        deleted[x]=1; //删除根对应的节点
        int now=merge(lson(x),rson(x));//合并
        root[lson(x)]=root[rson(x)]=root[x]=now;
        //因为并查集的路径压缩,很多点的父亲已经被压缩到x,所以要root[x]=now
        return val;
    }
    void ini(int *a,int n){
        for(int i=1;i<=n;i++){
            tree[i].val=a[i];
            root[i]=i;
            deleted[i]=0;
        }
    }
}T;
int n,m;
int a[MAXN+5];
int main(){
    int x,y,cmd;
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    T.ini(a,n);
    for(int i=1;i<=m;i++){
        scanf("%d",&cmd);
        if(cmd==1){
            scanf("%d %d",&x,&y);
            T.merge2(x,y);
        }else{
            scanf("%d",&x);
            printf("%d\n",T.deleteMin(x));
        }
    }
}

时间复杂度

一棵有 n个节点的二叉树,根的 dist 不超过\(\log(n)+1\) ,因为一棵根的dist为x的二叉树至少有x-1层是满二叉树,那么就至少有 \(2^x-1\)个节点。注意这个性质是所有二叉树都具有的,并不是左偏树所特有的。

对于左偏树,每次递归dist会-1 因为\(dist(x)=dist(rson(x))+1\) 所以合并的复杂度是\(O(\log n)\)

斜堆

不需要dist的限制

操作

跟左偏树一样先比较根 .每次合并后回溯,交换左右儿子 (个人感觉还是递归从下到上好理解,虽然画图的工作量大) (也有不递归的写法)

skew heap合并时总是交换当前节点的左右儿子,但当当前节点是右路径上的最大节点时,不交换左右儿子。这个问题主要会影响合并的最后一步,最后一步的根节点是右路径上最大的(或者说它没有右子树)则不交换。

int merge(int x,int y){
    if(!x||!y) return x+y;
    if(tree[x].val>tree[y].val) swap(x,y);
    rson(x)=merge(rson(x),y);//优先合并右儿子
    swap(lson(x),rson(x));
    return x;
}

时间复杂度

参考

对于节点p,如果p的右子树的大小>=p子树大小/2, 就定义p为heavy node. 否则为light node

当p的右子树发生合并(包括交换)

  • 如果p是heavy, 那么p一定变成light.(因为右子树变的更大,交换后到了左边)
  • 如果p是light,那么p可能变成heavy
  • 合并过程中,如果一个节点的 heavy/light 发生变化,那么它原先一定在堆的最右侧路径上 (因为我们是沿着根节点一直往右走的, 不管是x还是y)

设在右侧路径上的heavy node数量为\(h_x,h_y\), light node为\(l_x,l_y\) 整棵树不在右侧路径的heavy node数量为\(h^0_x,h^0_y\)

定义势函数\(\Phi\)为当前树中 heavy node的总数量

\(c=t_{worst}+\Phi(H_{new})-\Phi(H_x)-\Phi(H_y)\)

  • \(t_{worst}\)为真实的cost ( 就是模板中的\(a_i\) ) 最坏情况下是两树的右侧路径长度 \(h_x+l_x+h_y+l_y\)
  • \(\Phi(H_{new})=h_x^0+h_y^0+h'_x+h'_y \leq h_x^0+h_y^0+l_x+l_y\). 因为原来的\(h_x,h_y\)全部变成light了,而\(l_x,l_y\)有可能变成heavy

Comments