首页 > 其他分享 >左偏树

左偏树

时间:2023-01-08 11:33:21浏览次数:50  
标签:rt dist val int son include 左偏

一. 定义与性质

1.外节点: 一棵二叉树中左儿子或右儿子为空的节点称为外节点。

2.左偏树(Leftist Tree) 是一种可并堆的实现。左偏树是一棵二叉树,每个节点维护的值有:左右儿子,键值val和dist。

其中键值val用于比较节点的大小,dist表示此节点到其子树中最近的外节点的距离,用于维护左偏的性质。特别的,外节点的dist为0,空节点的dist为-1

左偏树的基本性质:

[堆性质] 一个节点的键值小于等于(或大于等于)其左右节点的键值

满足此条性质,我们就可以在\(O(1)\) 时间内完成取最小值或最大值的操作。

[左偏性质] 一个节点的左儿子的dist大于等于右儿子的dist

[性质3] 一个节点的dist等于其右儿子的dist + 1

[性质4] 一颗dist为\(k\)的左偏树至少有 \(2^{k + 1} - 1\) 个节点

推论:一颗n个节点的左偏树的dist最多为 \(\lfloor log(n + 1) - 1 \rfloor\)

二. 基本操作

合并操作(merge)

若不考虑左偏性质,合并两个堆(以小根堆为例)x, y分为以下几步:

1.交换x,y使得\(val_x < val_y\)

2.递归合并y与x的一个儿子,更新儿子信息

3.直到x,y其中一个为空停止

这样合并时间复杂度并不稳定,若合并的两个树都为链,时间复杂度将退化为\(O(n)\)

因此我们第二步合并时选择dist更小的x的右节点,设x和y的节点数分别为\(N_x\)和 \(N_y\), 由性质四的推论可知,这样合并的时间复杂度为 \(O(log N_x + log N_y)\)

合并之后我们需要继续维护左偏性质

int merge(int x, int y) {
	if(!x || !y) return x + y; //其中一个为空,返回另一个
	if(val[x] > val[y]) swap(x, y);
	son[x][1] = merge(son[x][1], y); //x的值更小,把y合并到x的右子树上
	fa[son[x][1]] = x; //根据题目确定是否需要并查集
	if(dist[son[x][1]] > dist[son[x][0]]) swap(son[x][0], son[x][1]); //维护左偏的性质
	dist[x] = dist[son[x][1]] + 1; 
	return x; 
}

取最值

取出堆顶键值即可

插入值

将单个的节点看作一个堆进行合并操作

删除堆顶

将堆顶节点的左右儿子合并即可

删除任意节点

先将其左右儿子合并,然后向上更新dist,直到不需要更新

三.模板

模板P3377 【模板】左偏树(可并堆)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <map>
using namespace std;
const int N = 100005;
int son[N][2], dist[N], val[N], fa[N];
int n, m;
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(val[x] > val[y] || (val[x] == val[y] && x > y)) swap(x, y);
	son[x][1] = merge(son[x][1], y); //x的值更小,把y合并到x的右子树上
	fa[son[x][1]] = x; 
	if(dist[son[x][1]] > dist[son[x][0]]) swap(son[x][0], son[x][1]); //维护左偏的性质
	dist[x] = dist[son[x][1]] + 1; 
	return x; 
}
void pop(int x) {
	val[x] = -1; 
	fa[son[x][0]] = fa[son[x][1]] = fa[x] = merge(son[x][0], son[x][1]);
}
int main() {
	// freopen("data.in", "r", stdin);
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) {
		scanf("%d", &val[i]);
	}
	for(int i = 0; i <= n; i++) fa[i] = i;
	dist[0] = -1;
	for(int i = 1; i <= m; i++) {
		int opt, x, y; scanf("%d", &opt);
		if(opt == 1) {
			scanf("%d%d", &x, &y);
			if(val[x] == -1 || val[y] == -1) continue;
			int fx = find(x), fy = find(y);
			if(fx == fy) continue;
			fa[fx] = fa[fy] = merge(fx, fy);
		} else {
			scanf("%d", &x);
			int fx = find(x);
			if(val[x] == -1) {
				printf("-1\n"); continue;
			}
			printf("%d\n", val[fx]);
			pop(fx);
		}
	}
	return 0;
}

四.其他操作

整堆修改

类似于线段树的lazy标记,整堆进行加法或乘法修改时只需要修改堆顶元素,然后打上标记,在进行其他操作时将标记下传即可。

例: P3261 [JLOI2015]城池攻占

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <map>
using namespace std;
typedef long long lld;
const int N = 300005;
struct Knight {
	int st_dep; lld val;
};
struct Heap {
	Knight kt;
	int son[2], dist;
	lld mul, delt;
} h[N];
vector <int> e[N];
int st[N], ans1[N], ans2[N], dep[N];
lld v[N], opt[N]; 
int opt_type[N], n, m;
void pushdown(int x) {
	if(h[x].son[0]) {
		h[h[x].son[0]].mul *= h[x].mul; h[h[x].son[0]].delt *= h[x].mul;
		h[h[x].son[0]].delt += h[x].delt; 
		h[h[x].son[0]].kt.val *= h[x].mul; h[h[x].son[0]].kt.val += h[x].delt;
	}
	if(h[x].son[1]) {
		h[h[x].son[1]].mul *= h[x].mul; h[h[x].son[1]].delt *= h[x].mul; 
		h[h[x].son[1]].delt += h[x].delt;
		h[h[x].son[1]].kt.val *= h[x].mul; h[h[x].son[1]].kt.val += h[x].delt;
	}
	h[x].delt = 0; h[x].mul = 1;
}
int merge(int x, int y) {
	if(!x || !y) return x + y;
	pushdown(x); pushdown(y);
	if(h[x].kt.val > h[y].kt.val) swap(x, y);
	h[x].son[1] = merge(h[x].son[1], y);
	if(h[h[x].son[0]].dist < h[h[x].son[1]].dist) swap(h[x].son[0], h[x].son[1]);
	h[x].dist = h[h[x].son[1]].dist + 1;
	return x;
}
void dfs1(int x) {
	for(auto y : e[x]) {
		dep[y] = dep[x] + 1;
		dfs1(y);
	}
}
void dfs(int x) {
	int rt = st[x];
	for(auto y : e[x]) {
		dfs(y);
		if(!st[y]) continue;
		if(!rt) {
			rt = st[y]; continue;
		}
		rt = merge(rt, st[y]);
	}
	while(h[rt].kt.val < v[x] && rt) {
		ans1[x]++; ans2[rt] = h[rt].kt.st_dep - dep[x];
		pushdown(rt);
		h[rt].kt.val = -1;
		rt = merge(h[rt].son[0], h[rt].son[1]);
	}
	st[x] = rt;
	if(!st[x]) return ;
	pushdown(rt);
	if(!opt_type[x]) {
		h[rt].delt += opt[x]; h[rt].kt.val += opt[x];
	} else {
		h[rt].mul *= opt[x]; h[rt].delt *= opt[x]; h[rt].kt.val *= opt[x];
	}
}
int main() {
	// freopen("data1.in", "r", stdin);
	// freopen("data.out", "w", stdout);
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) {
		scanf("%lld", &v[i]);
	}
	for(int i = 2, fa; i <= n; i++) {
		scanf("%d%d%lld", &fa, &opt_type[i], &opt[i]);
		e[fa].push_back(i);
	}
	dfs1(1); h[0].dist = -1;
	for(int i = 1; i <= m; i++) {
		lld start; int pos; scanf("%lld%d", &start, &pos);
		h[i].kt = (Knight){dep[pos], start};
		h[i].mul = 1;
		if(st[pos]) st[pos] = merge(st[pos], i);
		else st[pos] = i;
	}
	dfs(1);
	for(int i = 1; i <= n; i++) {
		printf("%d\n", ans1[i]);
	}
	for(int i = 1; i <= m; i++) {
		if(h[i].kt.val != -1) ans2[i] = h[i].kt.st_dep + 1;
		printf("%d\n", ans2[i]);
	}
	return 0;
}

五.其他例题

论文题:P4331 [BalticOI 2004]Sequence 数字序列
《黄源河 -- 左偏树的特点及其应用》

标签:rt,dist,val,int,son,include,左偏
From: https://www.cnblogs.com/mcggvc/p/17034317.html

相关文章

  • 左偏树 学习笔记
    左偏树是一类拥有下列操作的数据结构:插入一个数\(O(logn)\)求最小值\(O(1)\)删除一个节点\(O(logn)\)合并两棵树\(O(logn)\)左偏树本身具有堆的性质......
  • [可并堆] 左偏树
    左偏树0x00绪言左偏树是一种比较神奇的数据结构,代码实现类似于线段树,但又是一种原理和线段树完全不一样的数据结构,如果读者打算阅读此博客,一定要读完,不要只看前半部分分......
  • 2714. 左偏树
    题目链接2714.左偏树你需要维护一个小根堆的集合,初始时集合是空的。该集合需要支持如下四种操作:1a,在集合中插入一个新堆,堆中只包含一个数\(a\)。2xy,将第\(x\)......
  • LuoguP3377 【模板】左偏树(可并堆)
    题意如题,一开始有\(n\)个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:1xy:将第\(x\)个数和第\(y\)个数所在的小根堆合并(若第\(x\)或第\(y\)个......