可并堆大学习
前言
- 起因是msjing学了 A* 后写了一道 \(K\) 短路,发现还有一道 \(K\) 短路,但是需要可持久化可并堆,正好安排学可持久化 \(DS\) 于是学一下
可并堆
- 可并堆,
顾名思义就是可并的堆,是因为二叉堆的过于简单的性质使其无法做太多操作,C++ 自带的优先队列就支持堆顶加数,查堆顶,弹堆顶,手写无非多了一个下标删除,还是太有限,如果你合并俩堆,那需要 \(O(n \log n)\),太man了,所以可并堆就可以高效维护堆合并,比如斐波那契堆可以均摊 \(O(1)\) 合并,部分可并堆可以可持久化 - 可并堆有许多种:斜堆,左偏树,二项堆,配对堆,随机堆,斐波那契堆,各有优势
- msjing打算先学左偏树,因为可以可持久化
左偏树
- 左偏树,
顾名思义就是左偏的树 - 并不是,左偏树并不是一定左偏,仅仅是有左偏性质,左偏树也可以长得很平衡
- 其实你学了 \(FHQ\>\>Treap\) 后再学就很容易了,没学过也没事,~~毕竟反过来先学左偏树后 FHQ 就好学了
- 学习笔记
- 介绍左偏树,必须介绍它的左偏性质
左偏性质
- 我们定义:
- 一个节点为外节点当且仅当这个节点的儿子小于两个
- 一个节点的 \(dist\) 为其到子树中最近的外节点所经过的边的数量,即最近外节点的距离,定义空节点的 \(dist = -1\)
- 这个定义在许多地方不一样,有点地方定义空节点的 \(dist\) 为 \(0\),这里按这个说
- 画张图

- 我们依据定义,\(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要开始学可持久化并查集了啊啊啊啊啊

浙公网安备 33010602011771号