可并堆大学习

前言

  • 起因是msjing学了 A* 后写了一道 \(K\) 短路,发现还有一道 \(K\) 短路,但是需要可持久化可并堆,正好安排学可持久化 \(DS\) 于是学一下

可并堆

  • 可并堆,顾名思义就是可并的堆,是因为二叉堆的过于简单的性质使其无法做太多操作,C++ 自带的优先队列就支持堆顶加数,查堆顶,弹堆顶,手写无非多了一个下标删除,还是太有限,如果你合并俩堆,那需要 \(O(n \log n)\),太man了,所以可并堆就可以高效维护堆合并,比如斐波那契堆可以均摊 \(O(1)\) 合并,部分可并堆可以可持久化
  • 可并堆有许多种:斜堆,左偏树,二项堆,配对堆,随机堆,斐波那契堆,各有优势
  • msjing打算先学左偏树,因为可以可持久化

左偏树

  • 左偏树,顾名思义就是左偏的树
  • 并不是,左偏树并不是一定左偏,仅仅是有左偏性质,左偏树也可以长得很平衡
  • 其实你学了 \(FHQ\>\>Treap\) 后再学就很容易了,没学过也没事,~~毕竟反过来先学左偏树后 FHQ 就好学了
  • 学习笔记
  • 介绍左偏树,必须介绍它的左偏性质

左偏性质

  • 我们定义:
    • 一个节点为外节点当且仅当这个节点的儿子小于两个
    • 一个节点的 \(dist\) 为其到子树中最近的外节点所经过的边的数量,即最近外节点的距离,定义空节点的 \(dist = -1\)
  • 这个定义在许多地方不一样,有点地方定义空节点的 \(dist\)\(0\),这里按这个说
  • 画张图

2026-08-17 21-29-27屏幕截图

  • 我们依据定义,\(dist_3,dist_4,dist_5,dist_6\) 都为 \(0\)\(dist_1,dist_2\) 都为 \(1\)
  • 可能由于定义不同而导致不一致,但其实仅需要知道 \(dist\) 是什么就行,我们就只是用它来定义左偏
  • 左偏性质就是:每个节点左儿子的 \(dist\) 都不小于右儿子的 \(dist\)
  • 其实你多搓几张图就发现并不是所有的左偏树长得都是左偏的,而且有的左偏树也不一定平衡,因为链也符合定义
  • 有一个结论,是下面操作的核心:左偏树中,一个节点的 \(dist\) 等于它的右儿子的 \(dist + 1\)
  • 其实比较显然:由于左偏树的左偏性质,所以其左儿子的 \(dist\) 一定不会比右儿子小,所以小的在右儿子,而且由于 \(dist\) 的定义,所以节点的 \(dist\) 必会从 \(dist\) 小的右儿子转移,而且为其 \(dist + 1\),即:

\[dist_p = dist_{rs} + 1 \]

核心操作

  • 左偏树的核心操作是 \(merge\)(合并)
  • 我们要合并两个堆(接下来都默认小根堆),钦定堆顶大的堆向小的堆合并
  • 然后对于堆顶小的堆 \(x\),将其右儿子与堆顶大的堆 \(y\) 递归合并
  • 坏了这块学过 FHQ 一看就懂,但是没学过理解就很费劲
  • 给个代码理解一下
code
int merge(int x,int y)
{
    if (!x || !y) return x|y;
    if (v[x]>v[y])
        swap(x,y);
    rs[x]=merge(rs[x],y);
    if (dist[ls[x]]<dist[rs[x]]) swap(ls[x],rs[x]);
    dist[x]=dist[rs[x]]+1;
    return x;
}
  • 我们考虑堆顶的询问,如果从 \(x\) 一个一个上跳太man了,如果是一条链就是 \(O(n)\) 的,那我们可以用并查集的方法,路径压缩跳,巨快
  • 对了记得在合并堆后更新 \(fa\) 哦,msjing忘了呢,e,下面也要记得
code fid
int fid(int x) {return fa[x] == x ? x : fa[x]=fid(fa[x]);}
code merge+
  • 函数
int merge(int x,int y)
{
    if (!x || !y) return x|y;
    if (v[x]>v[y])
        swap(x,y);
    rs[x]=merge(rs[x],y);
    if (dist[ls[x]]<dist[rs[x]]) swap(ls[x],rs[x]);
    dist[x]=dist[rs[x]]+1;
    fa[x]=fa[ls[x]]=fa[rs[x]]=x;
    return x;
}
  • 下面
x=fid(x),y=fid(y);
if (x!=y) fa[x]=fa[y]=merge(x,y);

其他操作

查堆顶

  • 上面说了,就是 \(fid\)

加入新节点

  • 加入相当于是一个堆和一个仅有一个节点的堆合并

弹堆顶

  • 先把堆顶的值设为 \(-1\) 表示被踢了
  • 然后在将其左右儿子合并
code
void pop(int x)
{
    v[x].v=-1;
    fa[ls[x]]=ls[x],fa[rs[x]]=rs[x];
    fa[x]=merge(ls[x],rs[x]);
}

复杂度

  • 合并 \(O(\log n+\log m)\)\(n,m\) 分别是两个堆的大小
  • 证明看这

例题

P3377 【模板】可并堆 1

  • 板子,操作上面都说了
点击查看代码
#include <bits/stdc++.h>
using namespace std;
constexpr int maxn=1e5+10;
int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || ch>'9')
    {
        if (ch == '-') f=-1;
        ch=getchar();
    }
    while (ch>='0' && ch<='9')
    {
        x=(x<<1)+(x<<3)+ch-'0';
        ch=getchar();
    }
    return x*f;
}
int n,m;
namespace lt
{
    int dist[maxn],ls[maxn],rs[maxn],fa[maxn];
    struct _
    {
        int v,id;
        bool operator > (const _ &a) const
        {return v == a.v ? id>a.id : v>a.v;}
    }v[maxn];
    int fid(int x) {return fa[x] == x ? x : fa[x]=fid(fa[x]);}
    int merge(int x,int y)
    {
        if (!x || !y) return x|y;
        if (v[x]>v[y])
            swap(x,y);
        rs[x]=merge(rs[x],y);
        if (dist[ls[x]]<dist[rs[x]]) swap(ls[x],rs[x]);
        dist[x]=dist[rs[x]]+1;
        fa[x]=fa[ls[x]]=fa[rs[x]]=x;
        return x;
    }
    void pop(int x)
    {
        v[x].v=-1;
        fa[ls[x]]=ls[x],fa[rs[x]]=rs[x];
        fa[x]=merge(ls[x],rs[x]);
    }
}using namespace lt;
int main()
{
    n=read(),m=read();
    for (int i=1;i<=n;i++)
    {
        int x=read();
        v[i]={x,i},fa[i]=i;
    }
    dist[0]=-1;
    while (m--)
    {
        int op=read();
        if (op == 1)
        {
            int x=read(),y=read();
            if (v[x].v == -1 || v[y].v == -1) continue;
            x=fid(x),y=fid(y);
            if (x!=y) fa[x]=fa[y]=merge(x,y);
        }
        else
        {
            int x=read();
            if (v[x].v == -1) puts("-1");
            else x=fid(x),printf("%d\n",v[x].v),pop(x);
        }
    }
    return 0;
}

P2713 罗马游戏

  • 双倍经验,直接写

左偏树的可持久化

  • 其实可持久化是左偏树的重要应用
  • 由于左偏树的合并操作,所以可以做可持久化
  • 可持久化可并堆一般用于解决 \(K\) 短路问题,例子
  • 如何实现
  • 其实和其他可持久化的做法一致,就是在合并时候再复制一份合并
  • 就,没了

后记

  • 其实用的最多的是左偏树,其他没啥的
  • 其实可并堆基本没啥应用,一般上堆就是优先队列了
  • 其他堆以后补,msjing要开始学可持久化并查集了啊啊啊啊啊
posted @ 2026-08-18 10:19  msjing  阅读(9)  评论(0)    收藏  举报