可并堆 / 左偏树
可并堆 / 左偏树
左偏树
外节点:只有一个儿子或没有儿子的节点。
距离:一个节点到离他最近的外节点的距离,即两节点之间的路径权值和。特别地,外节点的距离为 0,空节点的距离为 −1.
左偏树:满足如下性质的二叉树:
- 堆性质:任何节点的权值小于等于儿子节点的权值,即 valfai≤vali.
- 左偏性质:任何节点的左儿子距离大于等于右儿子距离,即 dlsi≥drsi.
推论:
- 任意节点的距离等于右儿子距离 +1,即 di=drsi+1.
- 根节点的距离为 d 的左偏树,节点数不少于 2d+1−1.
操作
合并(Merge)
合并根节点为 x,y 的两颗左偏树。
- 假设 valx≤valy,那么根节点一定为 x,否则为 y.
- 需要递归合并 rsx 和 y,并将新树的根作为 rsx.
- 合并完成后,drsx 可能会变,为了保证左偏性质,若 dlsx<drsx,那么交换 x 的左右儿子。
- 更新 dx,以 x 为合并后的根节点。
int merge(int x, int y)
{
if(!x || !y) return x + y; // 若一个堆为空则返回另一个堆
if(val[x] > val[y]) swap(x, y); // 取值较小的作为根,满足堆性质
r[x] = merge(r[x], y), fa[r[x]] = x; // 递归合并右儿子与另一个堆
if(dis[l[x]] < dis[r[x]]) swap(l[x], r[x]); // 若不满足左偏性质则交换左右儿子
dis[x] = dis[r[x]] + 1;
return x;
}
插入(Push)
合并一个只有一个节点的左偏树。
弹出(Pop)
- 将根的左右节点合并,然后将合并后的节点设为根。
void pop(int x)
{
vis[x] = 1;
fa[l[x]] = l[x];
fa[r[x]] = r[x];
fa[x] = merge(l[x], r[x]);
l[x] = r[x] = dis[x] = 0;
return;
}
删除(Del)
- 将左右儿子合并后挂到父节点下,若父节点不满足左偏性质,则一路调整。
建树(Build)
- 暴力插入时间复杂度为 O(nlogn).
- 利用队列优化,两个两个合并,时间复杂度 O(n).
PBDS 实现
以下是一个可并的小根堆的实现(默认由配对堆实现,要优于左偏树):
#include <ext/pb_ds/priority_queue.hpp>
using namespace __gnu_pbds;
using namespace std;
const int N = 1e5 + 5;
priority_queue<int, less<int> > q[N];
对应的操作如下:
f = q[x].top(); // 获取堆顶(即最小值)
q[x].pop(); // 弹出堆顶
q[x].push(u); // 在堆 x 中插入 u
q[x].join(y); // 合并 x, y 两堆到 x
例题
P2713 罗马游戏
基本是模板题,注意由于合并后以 x 为根的并查集仍然会跳到 x,fax 也要设为 merge(x,y).
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, m, x, y, fa[N], val[N], dis[N], l[N], r[N];
bool died[N];
char opt;
int find(int x)
{
return (fa[x] == x ? x : fa[x] = find(fa[x]));
}
int merge(int x, int y)
{
if(!x || !y) return x + y;
if(val[x] > val[y]) swap(x, y);
r[x] = merge(r[x], y), fa[r[x]] = x;
if(dis[l[x]] < dis[r[x]]) swap(l[x], r[x]);
dis[x] = dis[r[x]] + 1;
return x;
}
int main()
{
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> val[i];
fa[i] = i, dis[i] = 0;
}
cin >> m;
while(m--)
{
cin >> opt >> x;
if(opt == 'M')
{
cin >> y;
if(died[x] || died[y]) continue;
int fx = find(x), fy = find(y);
if(fx != fy) fa[fx] = fa[fy] = merge(x, y);
}
else
{
if(died[x]) cout << "0\n";
else
{
int fx = find(x);
died[fx] = 1;
fa[fx] = fa[l[fx]] = fa[r[fx]] = merge(l[fx], r[fx]);
l[fx] = r[fx] = dis[fx] = 0;
cout << val[fx] << '\n';
}
}
}
return 0;
}
P1552 [APIO2012] 派遣
建一个大根堆,在遍历整棵树的过程中不断合并子树,并将费用最高的忍者依次弹出,直到总费用不超过预算。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 5;
int n, m, x, y, fa[N], val[N], dis[N], l[N], r[N], sum[N], siz[N], _b, _c, _l, ans;
struct ninja
{
int fth, cost, leading;
} a[N];
int find(int x)
{
return (fa[x] == x ? x : fa[x] = find(fa[x]));
}
int merge(int x, int y)
{
if(!x || !y) return x + y;
if(val[x] < val[y]) swap(x, y);
r[x] = merge(r[x], y), fa[r[x]] = x;
if(dis[l[x]] < dis[r[x]]) swap(l[x], r[x]);
dis[x] = dis[r[x]] + 1;
return x;
}
int pop(int x)
{
int ls = l[x], rs = r[x];
fa[ls] = ls, fa[rs] = rs;
l[x] = r[x] = dis[x] = 0;
return merge(ls, rs);
}
vector<int> g[N];
void dfs(int u)
{
for(auto v : g[u])
{
dfs(v);
fa[u] = merge(fa[u], fa[v]);
sum[u] += sum[v], siz[u] += siz[v];
}
while(sum[u] > m)
{
siz[u]--, sum[u] -= a[fa[u]].cost;
fa[u] = pop(fa[u]);
}
ans = max(ans, a[u].leading * siz[u]);
return;
}
signed main()
{
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
dis[0] = -1;
for(int i = 1; i <= n; i++)
{
cin >> a[i].fth >> a[i].cost >> a[i].leading;
fa[i] = i, val[i] = sum[i] = a[i].cost, siz[i] = 1;
if(a[i].fth) g[a[i].fth].push_back(i);
}
dfs(1);
cout << ans;
return 0;
}
P1456 Monkey King
对于每次操作,将两个堆顶的值除以 2,在弹出后重新插入它们,并合并两堆即可。
在这里作为 PBDS 写法的演示,亦可使用可并堆维护。
#include <bits/stdc++.h>
#include <ext/pb_ds/priority_queue.hpp>
using namespace std;
const int N = 1e5 + 5;
int n, m, s[N], fa[N], x, y;
__gnu_pbds :: priority_queue<int, less<int> > q[N];
int find(int x)
{
return (x == fa[x] ? x : fa[x] = find(fa[x]));
}
int main()
{
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
while(cin >> n)
{
for(int i = 1; i <= n; i++)
{
q[i].clear();
cin >> s[i];
fa[i] = i, q[i].push(s[i]);
}
cin >> m;
while(m--)
{
cin >> x >> y;
x = find(x), y = find(y);
if(x == y) {cout <<"-1\n"; continue;}
int cx = q[x].top(), cy = q[y].top();
q[x].pop(), q[y].pop();
cx /= 2, cy /= 2;
q[x].push(cx), q[y].push(cy);
q[x].join(q[y]), fa[y] = x;
cout << q[x].top() << '\n';
}
}
return 0;
}
望穿寂夜晨曦至,雄鹰展翅图九天。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效