可并堆 / 左偏树

可并堆 / 左偏树

左偏树

外节点:只有一个儿子或没有儿子的节点。

距离:一个节点到离他最近的外节点的距离,即两节点之间的路径权值和。特别地,外节点的距离为 \(0\),空节点的距离为 \(-1\).

image

左偏树:满足如下性质的二叉树:

  1. 堆性质:任何节点的权值小于等于儿子节点的权值,即 \(val_{fa_i} \le val_i\).
  2. 左偏性质:任何节点的左儿子距离大于等于右儿子距离,即 \(d_{ls_i} \ge d_{rs_i}\).

推论:

  1. 任意节点的距离等于右儿子距离 \(+ 1\),即 \(d_i = d_{rs_i} + 1\).
  2. 根节点的距离为 \(d\) 的左偏树,节点数不少于 \(2^{d+1} - 1\).

操作

合并(Merge)

合并根节点为 \(x, y\) 的两颗左偏树。

  • 假设 \(val_x \le val_y\),那么根节点一定为 \(x\),否则为 \(y\).
  • 需要递归合并 \(rs_x\)\(y\),并将新树的根作为 \(rs_x\).
  • 合并完成后,\(d_{rs_x}\) 可能会变,为了保证左偏性质,若 \(d_{ls_x} < d_{rs_x}\),那么交换 \(x\) 的左右儿子。
  • 更新 \(d_x\),以 \(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\left(n \log n\right)\).
  • 利用队列优化,两个两个合并,时间复杂度 \(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\)\(fa_x\) 也要设为 \(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;
}
posted @ 2024-07-01 09:37  心灵震荡  阅读(3)  评论(0编辑  收藏  举报