AcWing 2714:左偏树 ← 可并堆

【题目来源】
https://www.acwing.com/problem/content/2716/

【题目描述】
你需要维护一个小根堆的集合,初始时集合是空的。
该集合需要支持如下四种操作:
● 1 a,在集合中插入一个新堆,堆中只包含一个数 a。
● 2 x y,将第 x 个插入的数和第 y 个插入的数所在的小根堆合并。数据保证两个数均未被删除。若两数已在同一堆中,则忽略此操作。
● 3 x,输出第 x 个插入的数所在小根堆的最小值。数据保证该数未被删除。
● 4 x,删除第 x 个插入的数所在小根堆的最小值(若最小值不唯一,则优先删除先插入的数)。数据保证该数未被删除。

【输入格式】
第一行包含整数 n,表示总操作数量。
接下来 n 行,每行包含一个操作命令,形式如题所述。

【输出格式】
对于每个操作 3,输出一个整数,占一行,表示答案。

【输入样例】
6
1 3
1 2
2 1 2
3 1
4 2
3 1

【输出样例】
2
3

【数据范围】
1≤n≤2×10^5,
1≤a≤10^9,
1≤x,y≤当前插入数的个数。
数据保证所有操作合法。

【算法分析】
● 左偏树
(1)左偏树是一种可合并的堆(也叫 "可并堆"),它在普通二叉堆(大根 / 小根堆)的基础上,增加了「快速合并两个堆」的能力,同时保证堆的性质(小根堆:父节点≤子节点;大根堆反之)。普通二叉堆合并两个堆的时间复杂度是 O(n),而左偏树能做到 O(logn),这是它的核心优势。
(2)STL 未提供左偏树的实现,必须手动实现(指针 / 结构体 + 递归)。

● 左偏树的距离
左偏树的距离(dist)是其核心定义,用于保证左偏性质与合并效率。
一、先定义 "外节点"(External Node)
外节点:左子树为空或右子树为空的节点(叶子节点一定是外节点)。

二、节点距离 dist (x) 的定义
对任意节点 x:
若 x 是外节点:dist(x) = 0
若 x 不是外节点:dist (x) = 从 x 到其后代中最近的外节点所经过的边数
空节点(NULL):dist(NULL) = -1(约定)
三、左偏树的距离(整棵树的距离)
整棵左偏树的距离 = 根节点的 dist 值。
四、关键性质(由左偏性导出)
左偏树满足:对任意节点 x,dist (left (x)) ≥ dist (right (x))。
由此可推出:dist(x) = dist(right(x)) + 1
也就是说:节点的距离等于其右子树的距离 + 1。
这是左偏树合并、维护时最常用的公式。

● 左偏树习题集
https://www.luogu.com.cn/problem/P3377
https://www.luogu.com.cn/problem/P2713
https://www.luogu.com.cn/problem/P1456
https://www.luogu.com.cn/problem/P3261
https://www.luogu.com.cn/problem/P1552
https://www.luogu.com.cn/problem/P3273
https://www.luogu.com.cn/problem/P4331

【算法代码】

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N=2e5+5;
int v[N],dist[N],le[N],ri[N],idx;
int pre[N];
int n;

bool cmp(int x,int y) {
    if(v[x]!=v[y]) return v[x]<v[y];
    return x<y;
}

int find(int x) {
    if(x!=pre[x]) pre[x]=find(pre[x]);
    return pre[x];
}

int merge(int x,int y) { //Return the root node after merging
    if(!x || !y) return x+y;
    if(!cmp(x,y)) swap(x,y);
    ri[x]=merge(ri[x],y);
    if(dist[le[x]]<dist[ri[x]]) swap(le[x],ri[x]);
    dist[x]=dist[ri[x]]+1;
    return x;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    v[0]=INT_MAX;

    cin>>n;
    while(n--) {
        int op,x,y;
        cin>>op>>x;
        if(op==1) {
            v[++idx]=x;
            dist[idx]=0;
            pre[idx]=idx;
        } else if(op==2) {
            cin>>y;
            x=find(x),y=find(y);
            if(x!=y) {
                if(!cmp(x,y)) swap(x,y);
                pre[y]=x;
                merge(x,y);
            }
        } else if(op==3) cout<<v[find(x)]<<"\n";
        else {
            x=find(x);
            if(!cmp(le[x],ri[x])) swap(le[x],ri[x]);
            pre[x]=le[x],pre[le[x]]=le[x];
            merge(le[x],ri[x]);
        }
    }

    return 0;
}

/*
in:
6
1 3
1 2
2 1 2
3 1
4 2
3 1

out:
2
3
*/

【参考文献】
https://oi-wiki.org/ds/leftist-tree/
https://www.cnblogs.com/zdsrs060330/p/16095189.html
https://blog.csdn.net/hnjzsyjyj/article/details/158207666
https://www.acwing.com/solution/content/161640/
https://www.cnblogs.com/qkhm/p/LeftTree.html