左偏树

左偏树

\(\mathcal O(\log)\) 时间内合并两个堆,\(\mathcal O(1)\) 求堆顶。本文讨论的是小根堆。

一些定义

外结点:左儿子 / 右儿子为空的结点。

距离:\(x\) 的距离 \(dist_x\) 定义为其子树中与结点 \(x\) 最近的外结点到 \(x\) 的距离。特别的,空结点的距离为 \(-1\)

左偏树的性质

  1. 堆的性质,即 \(val_{lc} \geq val_x, val_{rc} \geq val_x\)
  2. 左偏性质,即 \(dist_{lc} \geq dist_{rc}\)

结论

  1. 由于性质 2,那么 \(dist_x =dist_{rc} + 1\)
  2. \(n\) 个结点的左偏树的根节点的距离是 \(\mathcal O(\log n)\) 的。
  3. 距离为 \(n\) 的左偏树至少有 \(2^{n+1} - 1\) 个结点。

2.3. 性质是所有二叉树都有的。

操作

合并操作

定义 \(merge(x, y)\) 表示合并两颗根节点分别为 \(x, y\) 的左偏树,返回值为合并后的根节点。

首先不考虑左偏性质,考虑如何合并两个堆:

  1. \(val_x \leq val_y\)\(x\) 作为合并后的根,否则 \(y\) 作为合并后的根。下文默认 \(val_x \leq val_y\)
  2. \(y\)\(x\) 的某一儿子合并,合并后的根节点作为 \(x\) 的对应儿子。
  3. 重复 1.2. 操作,直至 \(x,y\) 有一方为空结点。

上述方案复杂度为 \(\mathcal O(n)\),为使复杂度更加优秀,我们有两种方式:

  1. 每次随机选择 \(x\) 的左右儿子进行合并。
  2. 使用左偏树的性质。

这里主要讨论方式 2,考虑到左儿子距离大于等于右儿子,每次合并都选择 \(x\) 的右儿子与 \(y\) 进行合并,考虑到 \(dist\)\(\mathcal O(\log n)\) 级别的,故合并两颗大小为 \(a\)\(b\) 的左偏树的时间复杂度便为 \(\mathcal O(\log a + \log b)\)

每次合并之后可能会导致左偏性质的缺失,故每次合并之后判断是否有 \(dist_{lc} \geq dist_{rc}\),若无则交换 \(lc, rc\),后再更新 \(dist_x =dist_{rc} + 1\)

inline int merge(int x, int y)
{
	if (! x || ! y) return x + y;
	if (val[x] > val[y]) std::swap(x, y);
	rc[x] = merge(rc[x], y);
	if (dist[rc[x]] > dist[lc[x]])
		std::swap(lc[x], rc[x]);
	dist[x] = dist[rc[x]] + 1;
	return x;
}

插入操作

将单个元素看作一个堆,合并即可。

删除最小值

合并左右子树即可。

删除任意结点

将该结点的左右儿子合并,然后从该结点的父亲开始自底向上更新 \(dist\)、不满足左偏性质时交换左右儿子,当 \(dist\) 无需更新时结束递归。

复杂度分析:

令该结点父亲为 \(fa_x\),首先更新 \(dist_{fa_x}\),对于 \(fa_x\) 的祖先来说,更新时每次向上跳的前提是上次修改的结点是当前结点的右儿子,否则便会结束向上更新。而初始时 \(dist\)\(dist_{fa_x}\),每次向上跳都会使得 \(dist\) 增大,考虑到 \(dist\)\(\mathcal O(\log)\) 级别的,故更新是 \(\mathcal O(\log n)\) 的,故删除任意结点的复杂度为 \(\mathcal O(\log n)\)

整个堆加上/减去一个值、乘上一个正数

其实可以打标记且不改变相对大小的操作都可以。

在根打上标记即可,每次访问儿子时 pushdown。

判断两个元素所在堆是否已经合并

并查集维护即可。

已知初始 \(n\) 个元素,构建一颗左偏树可以做到 \(\mathcal O(n)\)

将每个结点放入双端队列,每次取队首两个元素进行合并,后将合并后元素插入队尾,可以证明时间时间复杂度为 \(\mathcal O(n)\)

注意事项

删除根结点后,根结点的 fa 需要修改为合并左右子树之后的根,因为路径压缩后根节点的儿子对应的 fa 可能指向根节点。

Link

#include <bits/stdc++.h>
const int N = 1e5 + 10;
inline int read()
{
	int cnt = 0; char ch = getchar(); bool op = 1;
	for (; ! isdigit(ch); ch = getchar())
		if (ch == '-') op = 0;
	for (; isdigit(ch); ch = getchar())
		cnt = cnt * 10 + ch - 48;
	return op ? cnt : - cnt;
}

int n, m;

int dist[N], val[N], lc[N], rc[N];
int fa[N];
inline int find(int x)
{
	if (fa[x] != x) return fa[x] = find(fa[x]);
	return fa[x];
}
inline int merge(int x, int y)
{
	if (! x || ! y) return x + y;
	if (val[x] > val[y] || (val[x] == val[y] && x > y)) 
		std::swap(x, y);
	rc[x] = merge(rc[x], y);
	if (dist[rc[x]] > dist[lc[x]])
		std::swap(lc[x], rc[x]);
	dist[x] = dist[rc[x]] + 1;
	return x;
}

int vis[N];

int main()
{
	n = read(), m = read();
	dist[0] = -1;
	for (int i = 1; i <= n; ++ i)
	{
		fa[i] = i; val[i] = read();
	}

	for (int i = 1; i <= m; ++ i)
	{
		int op, x, y;
		op = read();
		if (op == 1)
		{
			x = read(), y = read();
			int fax = find(x), fay = find(y);
			if (fax != fay && vis[x] == 0 && vis[y] == 0)
			{
				int allfa = merge(fax, fay);
				fa[fax] = fa[fay] = allfa;
			}
		}
		if (op == 2)
		{
			x = read(); 
			if (vis[x])
			{
				printf("-1\n");
			}
			else
			{
				int fax = find(x); dist[fax] = -1;
				vis[fax] = 1;
				printf("%d\n", val[fax]);
				int allfa = merge(lc[fax], rc[fax]);
				fa[fax] = fa[lc[fax]] = fa[rc[fax]] = allfa;
			}
		}
	}

	return 0;
}

参考文献

  1. OI-wiki
  2. https://www.luogu.com.cn/blog/hsfzLZH1/solution-p3377
posted @ 2022-03-04 00:05  chzhc  阅读(46)  评论(0编辑  收藏  举报
levels of contents