【洛谷P3377】【模板】左偏树(可并堆)
题目
题目链接:https://www.luogu.com.cn/problem/P3377
如题,一开始有 \(n\) 个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:
1 x y
:将第 \(x\) 个数和第 \(y\) 个数所在的小根堆合并(若第 \(x\) 或第 \(y\) 个数已经被删除或第 \(x\) 和第 \(y\) 个数在用一个堆内,则无视此操作)。2 x
:输出第 \(x\) 个数所在的堆最小数,并将这个最小数删除(若有多个最小数,优先删除先输入的;若第 \(x\) 个数已经被删除,则输出 \(-1\) 并无视删除操作)。
思路
定义左偏树中一个节点的深度 \(dis\) 为:
\[dis[x]=\left\{\begin{matrix}0\ (x\text{ 左子树或右子树为空})
\\\min(dis[lc_x],dis[rc_x])+1\ (\text{otherwise})
\end{matrix}\right.
\]
但是左偏树还需要满足堆性质以及任意一个节点左子树的深度不小于右子树的深度,这样一棵有 \(n\) 个节点的左偏树的最右链长度就是 \(O(\log n)\) 的,合并时复杂度也自然就是 \(O(\log n)\) 的了。
所以必然有 \(dis[x]=dis[rc_x]+1\),其中空节点的深度为 \(-1\)。
假设我们需要合并根分别为 \(x,y\) 的两个小根堆,我们先把值较小的设为 \(x\),然后继续合并 \(x\) 的右子树和 \(y\)。
但是我们需要知道一个节点所属的左偏树中的根节点,所以我们可以用并查集乱搞,合并的时候注意维护相关信息。
时间复杂度 \(O(n\log n)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
struct Lheap
{
int lc[N],rc[N],father[N],val[N],dis[N];
int find(int x)
{
return x==father[x]?x:father[x]=find(father[x]);
}
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);
rc[x]=merge(rc[x],y);
if (dis[lc[x]]<dis[rc[x]]) swap(lc[x],rc[x]);
father[lc[x]]=father[rc[x]]=father[x]=x;
dis[x]=dis[rc[x]]+1;
return x;
}
void del(int x)
{
father[lc[x]]=lc[x]; father[rc[x]]=rc[x];
father[x]=merge(lc[x],rc[x]);
val[x]=-1; lc[x]=rc[x]=0;
}
}lt;
int main()
{
// freopen("data.txt","r",stdin);
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
{
scanf("%d",<.val[i]);
lt.father[i]=i;
}
lt.dis[0]=-1;
while (m--)
{
int opt,x,y;
scanf("%d",&opt);
if (opt==1)
{
scanf("%d%d",&x,&y);
if (lt.val[x]==-1 || lt.val[y]==-1) continue;
x=lt.find(x); y=lt.find(y);
lt.father[x]=lt.father[y]=lt.merge(x,y);
}
else
{
scanf("%d",&x);
if (lt.val[x]==-1) printf("-1\n");
else
{
y=lt.find(x);
printf("%d\n",lt.val[y]);
lt.del(y);
}
}
}
return 0;
}