左偏树(左翼堆)

先附左偏树常见错误,警示自己:

  1. 开始时 dist[0]=1fa[i]=i

  2. 合并时更新右儿子的父亲。

  3. 两个返回值:空节点返回 x+y,否则返回 x

  4. pushup 先交换儿子,再判断是否为右儿子加 1

  5. 删除中间节点时是将这个节点的根和儿子树合并。

一般的二叉堆,要对其进行合并操作,最朴素的方法是每个元素逐一与另一个堆合并,即使是启发式,也是 O(n) 的复杂度(好像可以变成 log2n )。

然而,左偏树可以做到 O(logn) ,因为他并不是暴力合并,而是基于深度,而深度最多为 log

这里,我们引入一个变量 dist,指的是这个节点到叶子节点的距离。特别地,叶子节点的 dist0,空节点的 dist1

证明:一个有 n 个节点的左偏树,根节点(也就是 dist 最大的节点)的 dist 不大于 log(n+1)+1

假设根节点的 distk,那么树上最少有 2k+11 个节点,那么有 n>=2k+11

所以 log(n+1)+1>=k

这样一来,复杂度基于高度的合并就是正确的了。

左偏树的核心操作在于合并,而合并操作的核心在于 merge() 函数。

merge() 函数的参数是两个待合并的树(当然也是左偏树)的根节点,而返回值是合并后新的根节点。

这是一种递归式的操作。

每次对比两个参数,令更小的(视情况而定)作为新的根节点,较大的根节点再与新根节点的右儿子合并(因为树是左偏的,右子树才是决定 dist 的子树,保证 log 级别的复杂度),反复递归。

P3377 【模板】左偏树/可并堆

点击查看代码
#include <bits/stdc++.h>
using namespace std;
template <typename T> inline void read(T &x)
{
	x = 0; bool f = 0; char ch = getchar();
	while('0' > ch || ch > '9') { if(ch == '-') f = !f; ch = getchar(); }
	while('0' <= ch && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
	x = f ? -x : x; return;
}
template <typename T> inline void print(T x)
{
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) print(x / 10);
	putchar(x % 10 + '0'); return;
}
const int N = 1e5 + 10;
int n, m;
int a[N], fa[N], lc[N], rc[N], dist[N], del[N];
int find(int x)
{
	if(fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}
int merge(int x, int y)
{
	if(!x || !y) return x + y;
	if(a[x] > a[y] || a[x] == a[y] && x > y) swap(x, y);
	rc[x] = merge(rc[x], y);
	fa[rc[x]] = x;
	if(dist[lc[x]] < dist[rc[x]]) swap(lc[x], rc[x]);
	dist[x] = dist[rc[x]] + 1;
	return x;
}
void Pop(int x)
{
	del[x] = 1;
	fa[lc[x]] = lc[x], fa[rc[x]] = rc[x];
	fa[x] = fa[lc[x]] = fa[rc[x]] = merge(lc[x], rc[x]);
	lc[x] = rc[x] = dist[x] = 0;
}
int main()
{
	dist[0] = -1;
	read(n); read(m);
	for(int i = 1; i <= n; i ++) read(a[i]), fa[i] = i;
	int op, x, y;
	for(int i = 1; i <= m; i ++)
	{
		read(op);
		if(op == 1)
		{
			read(x); read(y);
			if(del[x] || del[y]) continue;
			x = find(x); y = find(y);
			if(x != y) fa[x] = fa[y] = merge(x, y);
		}
		else if(op == 2)
		{
			read(x);
			if(del[x]) print(-1), putchar('\n');
			else
			{
				x = find(x);
				print(a[x]);
				putchar('\n');
				Pop(x);
			}
		}
	}
	return 0;
}

接下来是几道例题。

P1456 Monkey King

这题与板题的不同点在于没有删除(其实有,但是后面又加进来了),但是注意合并操作比较多,不要遗漏

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int a[N], dist[N], lc[N], rc[N], fa[N];
int find(int x)
{
	if(fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}
int merge(int x, int y)
{
	if(!x || !y) return x + y;
	if(a[x] < a[y]) swap(x, y);
	rc[x] = merge(rc[x], y);
	fa[rc[x]] = x;
	if(dist[lc[x]] < dist[rc[x]]) swap(lc[x], rc[x]);
	dist[x] = dist[rc[x]] + 1;
	return x;
}
void Pop(int x)
{
	a[x] >>= 1;
	fa[lc[x]] = lc[x], fa[rc[x]] = rc[x];
	int rt;
	fa[lc[x]] = fa[rc[x]] = rt = merge(lc[x], rc[x]);
	lc[x] = rc[x] = dist[x] = 0;
	fa[x] = fa[rt] = merge(rt, x);
}
int main()
{
	while(scanf("%d", &n) != EOF)
	{
		dist[0] = -1;
		for(int i = 1; i <= n; i ++) scanf("%d", &a[i]), fa[i] = i;
		scanf("%d", &m);
		int x, y;
		for(int i = 1; i <= m; i ++)
		{
			scanf("%d %d", &x, &y);
			x = find(x); y = find(y);
			if(x == y)
			{
				printf("-1\n");
				continue;
			}
			Pop(x); Pop(y);
			x = find(x), y = find(y);
			fa[x] = fa[y] = merge(x, y);
			printf("%d\n", a[fa[x]]);
		}
		for(int i = 1; i <= n; i ++)
		{
			lc[i] = rc[i] = dist[i] = fa[i] = 0;
		}
	}
	return 0;
}

P4971 断罪者

这题多了一个修改中间值的操作,只要将这个数的子树都分离出去,改一下大树的结构( pushup 改左右子树, dist 大的是左,修改也是 logn ),再将数的两个儿子合并,再将儿子树与大树合并即可。

点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
template <typename T> inline void read(T &x)
{
	x = 0; bool f = 0; char ch = getchar();
	while('0' > ch || ch > '9') { if(ch == '-') f = !f; ch = getchar(); }
	while('0' <= ch && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
	x = f ? -x : x; return;
}
const int N = 2e6 + 10;
int T, W, K;
int n, m;
int a[N], lc[N], rc[N], fa[N], dist[N];
int find(int x)
{
	if(fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}
int merge(int x, int y)
{
	if(!x || !y) return x + y;
	if(a[x] < a[y] || a[x] == a[y] && x > y) swap(x, y);
	rc[x] = merge(rc[x], y);
	fa[rc[x]] = x;
	if(dist[lc[x]] < dist[rc[x]]) swap(lc[x], rc[x]);
	dist[x] = dist[rc[x]] + 1;
	return x;
}
void pushup(int x)
{
	if(!x) return;
	if(dist[lc[x]] < dist[rc[x]]) swap(lc[x], rc[x]);
	if(dist[x] != dist[rc[x]] + 1)
	{
		dist[x] = dist[rc[x]] + 1;
		pushup(fa[x]);
	}
}
void ins(int x)
{
	fa[lc[x]] = lc[x], fa[rc[x]] = rc[x];
	int rt;
	rt = fa[lc[x]] = fa[rc[x]] = merge(lc[x], rc[x]);
	lc[x] = rc[x] = dist[x] = 0;
	pushup(fa[x]);
	x = find(x);
	fa[x] = fa[rt] = merge(x, rt);
}
signed main()
{
	read(T); read(W); read(K);
	for(int ii = 1; ii <= T; ii ++)
	{
		dist[0] = -1;
		read(n); read(m);
		for(int i = 1; i <= n; i ++) read(a[i]), fa[i] = i;
		int op, x, y, mx = 0, sum = 0;
		for(int i = 1; i <= m; i ++)
		{
			read(op);
			if(op == 2)
			{
				read(x);
				a[x] = 0;
				ins(x);
			}
			else if(op == 3)
			{
				read(x); read(y);
				x = find(x);
				a[x] = max(a[x] - y, (int)0);
				ins(x);
			}
			else if(op == 4)
			{
				read(x); read(y);
				x = find(x); y = find(y);
				if(x != y) fa[x] = fa[y] = merge(x, y);
			}
		}
		for(int i = 1; i <= n; i ++)
		{
			int t = find(i);
			if(t != i) continue;
			sum += a[t];
			mx = max(mx, a[t]);
		}
		if(W == 2) sum -= mx;
		else if(W == 3) sum += mx;
		if(sum == 0) printf("Gensokyo");
		else if(sum > K) printf("Hell");
		else printf("Heaven");
		printf(" %lld\n", sum);
		for(int i = 0; i <= n; i ++) a[i] = lc[i] = rc[i] = dist[i] = fa[i] = 0;
	}
	return 0;
}
posted @   小队长wtz  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示