P3369 【模板】普通平衡树(FHQ Treap树构建和解析)

题目描述

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入 x 数
  2. 删除 x 数(若有多个相同的数,应只删除一个)
  3. 查询 x 数的排名(排名定义为比当前数小的数的个数 +1 )
  4. 查询排名为 x 的数
  5. 求 x 的前驱(前驱定义为小于 x,且最大的数)
  6. 求 x 的后继(后继定义为大于 x,且最小的数)

输入格式

第一行为 n,表示操作的个数,下面 n 行每行有两个数 opt 和 x,opt 表示操作的序号( 1≤opt≤6 )

输出格式

对于操作 3,4,5,6 每行输出一个数,表示对应答案

输入输出样例

输入 #1复制

10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

输出 #1复制

106465
84185
492737

说明/提示

【数据范围】
对于 100% 的数据,1≤n≤105,∣x∣≤107

构建树:

对FHQTreap树进行构建:

FHQ Treap树是最后形态由键值和优先级决定。它的高明之处是所有操作都只用到了分裂和合并这两个基本操作,这两个操作的复杂度都为O(log2 n)。

前置知识:

  • C++
  • 二叉搜索树的基本性质,下面会讲
  • 二叉堆

二叉树搜索树是左节点小于根节点,根节点小于右节点。

用中序遍历可以看到是一个单调递增的序列。

二叉堆:

堆有大根堆和小根堆之分。

大根堆是根节点>左节点and 根节点>右节点;小根堆相反;

大根堆和小根堆一般上一层是满二叉树。这时会提高搜索的效率,时间复杂度减少。O(log2n)

有了以上的知识点:

对题目进行分析:

1、二叉树需要有左右节点。

2、要有一个键值,随机变量。

3、左右节点+本身的大小(这时在排名上有很大的用处)

代码如下:

struct Node {
	int ls, rs;   // 左右 子节点 
	int key, pri; //  key为值 pri 为随机的优先级
	int size; // 当前节点为根的子树的节点数量,用于求第k大和排名
}t[M]; 

核心:

1.分裂,返回以L和R为根的两棵树

先看图:划分为两棵树,左子树小于x,右子树大于x

 上代码:

void Split(int u, int x, int& L, int& R) {
	if (u == 0) { //到达叶子,递归返回
		L = 0, R = 0;
		return;
	}

	if (t[u].key <= x) { // 本节点比x小,那么到右子树找x 
		L = u;           // 左树的根是本节点  // 下一个如果到 这来 上一个的rs 为 这个节点u ,因为u.rs的全部字节点都大于 u的值
		Split(t[u].rs, x, t[u].rs, R); // 通过rs传回新的子节点  
	}
	else {
		R = u; // 根节点 
		Split(t[u].ls, x, L, t[u].ls);
	}
	Update(u);
}

返回两棵树的树根L和R。

2.合并两颗子树

这是分裂的逆过程;用随机值进行可以确保树的高度比较小,这就是为什么要引入随机值的原因。

先以小根堆为例,如图所示合并两棵树。

 我这里用了大根堆为例:

int Merge(int L, int R) {
	if (L == 0 || R == 0) {
		return L + R; // 左 or  右节点 
	} // 建立大顶堆
	if (t[L].pri > t[R].pri) { // 合并树  随机值 到的在右边
		t[L].rs = Merge(t[L].rs, R); // 
		Update(L);
		return L;
	}
	else {
		t[R].ls = Merge(L, t[R].ls);
		Update(R);
		return R;
	}
}

权值大的在上面。

有着两种思想就好写多了。

代码解释请看注释

解题代码:

#include<iostream>
#include<cmath>
#include<stdlib.h>
using namespace std;
const int M = 1e6 + 10;
int cnt = 0, root = 0;
struct Node {
	int ls, rs;   // 左右 子节点 
	int key, pri; //  key为值 pri 为随机的优先级
	int size; // 当前节点为根的子树的节点数量,用于求第k大和排名
}t[M]; 
void newNode(int x) {
	cnt++;
	t[cnt].size = 1;
	t[cnt].ls = t[cnt].rs = 0;
	t[cnt].key = x;
	t[cnt].pri = rand();
}
void Update(int u) {
	t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;
}
void Split(int u, int x, int& L, int& R) {
	if (u == 0) { //到达叶子,递归返回
		L = 0, R = 0;
		return;
	}

	if (t[u].key <= x) { // 本节点比x小,那么到右子树找x 
		L = u;           // 左树的根是本节点  // 下一个如果到 这来 上一个的rs 为 这个节点u ,因为u.rs的全部字节点都大于 u的值
		Split(t[u].rs, x, t[u].rs, R); // 通过rs传回新的子节点  
	}
	else {
		R = u; // 根节点 
		Split(t[u].ls, x, L, t[u].ls);
	}
	Update(u);
}

int Merge(int L, int R) {
	if (L == 0 || R == 0) {
		return L + R; // 左 or  右节点 
	} // 建立大顶堆
	if (t[L].pri > t[R].pri) { // 合并树  随机值 到的在右边
		t[L].rs = Merge(t[L].rs, R); // 
		Update(L);
		return L;
	}
	else {
		t[R].ls = Merge(L, t[R].ls);
		Update(R);
		return R;
	}
}

void Insert(int x) {
	int L, R; // 左右根的节点
	Split(root, x, L, R); 
	newNode(x); //生成x 
	int aa = Merge(L, cnt); //合并节点,这里是生成的节点,合并左子树中
	root = Merge(aa, R); //两棵树进行合并
}
void Del(int x) {//删除节点
	int L, R, p;
	Split(root, x, L, R); //先抛出 左根的节点 小于等于x   右根  大于 x
 	Split(L, x - 1, L, p); //在进行抛 右节点一定为 x 以p为根 ,左节点一定小于 x的,
	p = Merge(t[p].ls, t[p].rs); //我们只需将连接左右儿子,根节点就会被抛弃 
	root = Merge(Merge(L, p), R); // 合并左右子树
}

void Rank(int x) {//计算x的排名
	int L, R;
	Split(root, x - 1, L, R);
	printf("%d\n", t[L].size + 1); //左节点 + 1
	root = Merge(L, R);
}

int kth(int u, int k) { //计算排名为k的点 这个要和上面弄清楚
	if (k == t[t[u].ls].size + 1) {
		return u;
	}
	if (k <= t[t[u].ls].size) {
		return kth(t[u].ls, k);
	}
	if (k > t[t[u].ls].size) {  // 这里是 ls 不是 r 遍历右子树
		return kth(t[u].rs, k - t[t[u].ls].size - 1);
	}
}
void Precursor(int x) {
	int L, R;
	Split(root, x - 1, L, R);
	printf("%d\n", t[kth(L, t[L].size)].key);//这个size是节点的个数 刚好就是排名最后的点的位置
	root = Merge(L, R);
}
void Successor(int x) { //右子树的第一个点
	int L, R;
	Split(root, x, L, R);
	printf("%d\n", t[kth(R, 1)].key); // 和上面同理
	root = Merge(L, R);
}

int main() {
	srand(time(NULL));
	int n;
	cin >> n;
	while (n--) {
		int opt, x;
		cin >> opt >> x;
		switch (opt) {
		case 1:Insert(x); break;
		case 2:Del(x); break;
		case 3:Rank(x); break;
		case 4:printf("%d\n", t[kth(root, x)].key); break;
		case 5:Precursor(x); break;
		case 6:Successor(x); break;
		}
	}
	return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可持久化splay是一种数据结构,它是对splay进行修改和查询的一种扩展。在传统的splay中,对的修改操作会破坏原有的结构,而可持久化splay则允许我们对进行修改、查询,并且可以保存修改后的每个版本的结构。 在可持久化splay中,我们不会直接对原进行修改,而是通过复制每个节点来创建新的版本。这样,每个版本都可以独立地修改和查询,保留了原有版本的结构和状态。每个节点保存了其左子和右子的引用,使得可以在不破坏原有版本的情况下进行修改和查询。 为了实现可持久化splay,我们可以使用一些技巧,比如引用中提到的哨兵节点和假的父节点和孩子节点。这些技巧可以帮助我们处理根节点的旋转和其他操作。 此外,可持久化splay还可以与其他数据结构相结合,比如引用中提到的可持久化线段。这种结合可以帮助我们解决更复杂的问题,比如区间修改和区间查询等。 对于可持久化splay的学习过程,可以按照以下步骤进行: 1. 理解splay的基本原理和操作,包括旋转、插入、删除和查找等。 2. 学习如何构建可持久化splay,包括复制节点、更新版本和保存历史版本等。 3. 掌握可持久化splay的常见应用场景,比如区间修改和区间查询等。 4. 深入了解与可持久化splay相关的其他数据结构和算法,比如可持久化线段等。 在解决问题时,可以使用二分法来确定答案,一般称为二分答案。通过对答案进行二分,然后对每个答案进行检查,以确定最终的结果。这种方法可以应用于很多问题,比如引用中提到的在线询问问题。 综上所述,可持久化splay是一种对splay进行修改和查询的扩展,可以通过复制节点来创建新的版本,并且可以与其他数据结构相结合解决更复杂的问题。学习过程中可以按照一定的步骤进行,并且可以使用二分法来解决一些特定的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [[学习笔记]FHQ-Treap及其可持久化](https://blog.csdn.net/weixin_34283445/article/details/93207491)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [可持久化数据结构学习笔记](https://blog.csdn.net/weixin_30376083/article/details/99902410)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值