左偏树

博主学习左偏树时阅读的博客

前置芝士

了解堆。

引言

如果要求你合并两个优先队列,并且合并后的结果仍然符合优先队列的性质,此时怎么做?
显然,此处不能使用普通的堆。那如何完成操作呢?这时候就需要使用一种特殊的堆了:左偏树。


正文

左偏树的名,乍一听,像是棵向左偏的树。实际上它的确是一棵向左偏的树(雾),但是向左偏的并不是节点。具体是什么“左偏”会在下文中讲解。

概念定义

外节点

当且仅当此节点的左子节点和右子节点中的一个是空节点,或者左子节点和右子节点都是空节点,该节点才能被称作外节点。

如图:A节点不属于外节点,因为它有左右子节点;B、C节点属于外节点,因为B节点没有右子节点,C节点没有左子节点;D节点也属于外节点,因为它没有左右子节点。

距离

左偏树中,点的距离为该点到它的子树中,离它最近的外节点的距离。

该图中,离根节点最近的外节点为 \(C\)\(A → B → C\) 需经过两个节点,所以根节点 \(A\) 的距离为2。


性质与推论

一颗左偏树,必须有如下两个性质:

· 性质一. 任意节点的权值都要小于其父节点(如果有的话)的权值;

· 性质二. 任意节点的左子节点的距离一定大于右子节点的距离;

根据性质,可以得到如下推论:

· 推论一. 任意一个非外节点的距离为其右子节点的距离 + 1;

· 推论二. \(n\) 个点的左偏树的最大距离为 \(log(n + 1) - 1。\)

推论一给出了更新节点距离的方法,而推论二保证了左偏树的时间复杂度。


操作

合并(merge)

快速合并两个普通堆基本是天方夜谭,但合并操作在左偏树中的重要性不亚于splay操作在Splay中的重要性,所以请读者务必理解这一模块。
假设现在给定两棵左偏树,\(a, b\) 是它们的根节点,现在要将 \(b\) 合并到 \(a\) 上。
根据堆最基础的性质(根节点的权值应小于子节点的权值),\(b\) 的权值应小于当前遍历到的点(以下称其为 \(x\) )。故如果遇到 \(val_x < val_b\) 的情况,则要将 \(x\)\(b\) 调换。

假设 \(val_x > val_b\) ,则此时应将 \(x\)\(b\) 调换。

调换完成后,继续向右子节点递归,直到当前节点没有右子节点、或者 \(b\) 被清空。回溯的过程中,更新左右子节点的距离并判断是否满足性质二,若不满足则需调换左右子节点,直到回溯到根节点。

inline int merge (int x, int y) { //合并两棵树 
	if (!x) //若该节点无左儿子,则返回右子树;下同 
		return y;
	if (!y)
		return x;
	if (val[x] > val[y] || (val[x] == val[y] && x > y)) //若左右节点的val值出现矛盾,则进行置换操作 
		swap (x, y);
	sons[x][1] = merge (sons[x][1], y); //从右子节点开始进一步进行合并操作 
	if (dis[sons[x][1]] > dis[sons[x][0]]) //若x,y节点的距离值矛盾,则置换 
		swap (sons[x][0], sons[x][1]);
	fa[sons[x][0]] = fa[sons[x][1]] = fa[x] = x; //路径压缩 
	dis[x] = dis[sons[x][1]] + 1; //更新距离值 
	return x;
}

加入单个节点(push/add)

将单个节点当成只有一个节点的左偏树进行合并。

inline int add (int value, int x) { //增加一个节点,以子树形式加入 
	cnt ++;
	sons[cnt][0] = sons[cnt][1] = dis[cnt] = 0;
	val[cnt] = value;
	return merge (cnt, x); //将单独的节点以子树形式合并入整棵树 
}

弹出根节点(pop/del)

清空根的信息,将根的左右子节点的父亲值改为自身,再进行合并。

inline void del (int x) { //删除根节点 
	int l = sons[x][0], r = sons[x][1];
	val[x] = -1;
	fa[l] = l;
	fa[r] = r;
	fa[x] = merge (l, r); //合并左右子节点 
	return ;
}

建立左偏树(build)

将所有节点压入一个队列,每次取出队列头的两个节点,进行合并后,将新根节点压入队列最末端,直到队列只剩一个元素(总根)。

inline int build () { //建造左偏树 
	queue <int> q;
	for (int i = 1; i <= n; i ++) //将节点压入序列中 
		q.push (i);
	while (!q.empty ())
		if (q.size () == 1)
			break ;
		else { //每次将队首的两个节点当作两颗子树进行合并,将得到的新子树再压入队列 
			int x = q.front (); q.pop ();
			int y = q.front (); q.pop ();
			q.push (merge (x, y));
		}
	int rt = q.front (); q.pop (); //更新根节点 
	return rt;
}

总代码

可以在 Luogu3377 上提交。

//例题并没有用到所有的函数,建议读者打完板子后,自行测试所有函数的正确性。 
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5, INF = INT_MAX;
int n, m, cnt, a[N], fa[N], dis[N], val[N], sons[N][2];
//cnt:当前节点数; fa[x]:x的父节点; dis[x]:x的距离; val[x]:x节点的值; sons[x][y]:x节点的y儿子 
inline int find (int x) { //寻找祖父节点 
	if (fa[x] != x)
		return fa[x] = find (fa[x]); //注意此处要路径压缩,否则会在最后一个点超时 
	return x;
}
inline int merge (int x, int y) { //合并两棵树 
	if (!x) //若该节点无左儿子,则返回右子树;下同 
		return y;
	if (!y)
		return x;
	if (val[x] > val[y] || (val[x] == val[y] && x > y)) //若左右节点的val值出现矛盾,则进行置换操作 
		swap (x, y);
	sons[x][1] = merge (sons[x][1], y); //从右子节点开始进一步进行合并操作 
	if (dis[sons[x][1]] > dis[sons[x][0]]) //若x,y节点的距离值矛盾,则置换 
		swap (sons[x][0], sons[x][1]);
	fa[sons[x][0]] = fa[sons[x][1]] = fa[x] = x; //路径压缩 
	dis[x] = dis[sons[x][1]] + 1; //更新距离值 
	return x;
}
inline int add (int value, int x) { //增加一个节点,以子树形式加入 
	cnt ++;
	sons[cnt][0] = sons[cnt][1] = dis[cnt] = 0;
	val[cnt] = value;
	return merge (cnt, x); //将单独的节点以子树形式合并入整棵树 
}
inline void del (int x) { //删除根节点 
	int l = sons[x][0], r = sons[x][1];
	val[x] = -1;
	fa[l] = l;
	fa[r] = r;
	fa[x] = merge (l, r); //合并左右子节点 
	return ;
}
inline int build () { //建造左偏树 
	queue <int> q;
	for (int i = 1; i <= n; i ++) //将节点压入序列中 
		q.push (i);
	while (!q.empty ())
		if (q.size () == 1)
			break ;
		else { //每次将队首的两个节点当作两颗子树进行合并,将得到的新子树再压入队列 
			int x = q.front (); q.pop ();
			int y = q.front (); q.pop ();
			q.push (merge (x, y));
		}
	int rt = q.front (); q.pop (); //更新根节点 
	return rt;
}
int opt, firs, seco;
int main () {
	scanf ("%d%d", &n, &m);
	dis[0] = -1;
	for (int i = 1; i <= n; i ++) {
		scanf ("%d", &val[i]);
		fa[i] = i;
	}
	for (int i = 1; i <= m; i ++) {
		scanf ("%d", &opt);
		if (opt == 1) {
			scanf ("%d%d", &firs, &seco);
			int xx = find (firs), yy = find (seco);
			if (val[firs] == -1 || val[seco] == -1 || xx == yy)
				continue ;
			else
				fa[xx] = fa[yy] = merge (xx, yy);
		} else {
			scanf ("%d", &firs);
			if (val[firs] == -1)
				puts ("-1");
			else {
				int rt = find (firs);
				printf ("%d\n", val[rt]);
				del (rt);
			}
		}
	}
	return 0;
}
posted @ 2020-07-09 20:10  HarryHuang  阅读(166)  评论(4编辑  收藏  举报