[C++ 学习笔记] FHQ-Treap (无旋 Treap)

1. 基本知识


1.1 Treap

Treap(树堆) 是一种弱平衡二叉搜索树,每个节点的权值具有二叉搜索树的性质,优先级具有堆的性质

具体来说,树堆满足如下性质

  • 左子结点的权值小于根节点的权值
  • 右子节点的权值大于根节点的权值
  • 子节点的优先级(注意,不是权值!)都大于/都小于根节点的优先级(取决于大根堆/小根堆)

如下图即为一个 Treap 的例子(采用小根堆)——

注:黑色数字为权值,棕色数字为优先级

为什么相较于 BST,需要对每个节点多维护一个优先级?

对每个节点,维护满足堆性质的优先级,是为了保证树尽量平衡:

对于一棵 每层节点较多 的二叉搜索树,查找时需要递归的次数便较小,复杂度为 \log_{2} n

反之,退化成一条链 是二叉搜索树的最坏情况,此时查找所需复杂度为线性,而优先级的设置便防止了这种情况的出现

1.2 FHQ-Treap

FHQ-Treap 又称分裂合并 Treap,无须旋转,只需支持分裂合并两种基本操作

FHQ-Treap 支持可持久化和区间操作,但此处仅介绍基本操作

2. 初始化 FHQ-Treap


在 FHQ-Treap 的节点上维护如下属性与方法(采用指针式):

  1. 左右子节点的地址
  2. 当前节点的权值
  3. 当前节点的优先级
  4. 当前节点的出现次数
  5. 当前节点的子树大小
  6. 重新计算子树大小的方法

于是可以得到节点的代码:

struct Node {
	Node *ch[2];    // 左右孩子的指针
	int val;    // 权值
	int rank;    // 优先级
	int rep_cnt;    // 出现次数
	int siz;    // 子树大小

	Node (int _val) : val(_val), rep_cnt(1), siz(1) {    // 构造函数
		ch[0] = ch[1] = nullptr;
		rank = rand();    // 随机取优先级
	}

	void upd_siz() {    // 重新计算子树大小的方法
		siz = rep_cnt;

		if (ch[0] != nullptr) {
			siz = siz + ch[0]->siz;    // 加上左子树的大小
		}

		if (ch[1] != nullptr) {
			siz = siz + ch[1]->siz;    // 加上右子树的大小
		}
	}
};

3. 分裂


3.1 按值分裂

分裂过程接收 根节点的指针 关键值 两个参数,返回两个指针,分别指向分裂后的 两个 Treap,第一个 Treap 所有节点的值小于等于关键值,第二个 Treap 所有节点的值大于关键值

对于要分裂的关键值 (key),大致分裂思路如下:

  1. 首先比较根节点的值与关键值
  2. 如果根节点的值小于等于关键值,说明根节点及其左子树上的所有节点均小于等于关键值,而此时根节点的右子树上仍可能有节点小于等于关键值,因此递归分裂右子树 (结果记为 temp),再将当前根节点的右孩子指向 temp.first,最终返回根节点的指针与temp.second
  3. 如果根节点的值大于关键值,说明根节点及其右子树上的所有节点均大于关键值,而此时根节点的左子树上仍可能有节点大于关键值,因此递归分裂左子树 (结果记为 temp),再将当前根节点的左孩子指向 temp.second,最终返回 temp.first 与根节点的指针

于是可以得到按值分裂的代码:

pair<Node *, Node *> split(Node *root, int key) {    // 根节点指针,关键值
    if (root == nullptr) {    // 当前节点不存在
		return {nullptr, nullptr};
	}

	if (root->val <= key) {    // 根节点的值小于等于关键值
		auto temp = split(root->ch[1], key);    // 递归分裂右子树
		root->ch[1] = temp.first;
		root->upd_siz();    // 别忘了更新子树大小
		return {root, temp.second};
	} else {    // 根节点的值大于关键值
		auto temp = split(root->ch[0], key);    // 递归分裂左子树
		root->ch[0] = temp.second;
		root->upd_siz();    // 别忘了更新子树大小
		return {temp.first, root};
	}
}

3.2 按排名分裂

排名定义为,比当前权值小的权值的个数 +1(注意重复的也算哦)

分裂过程接收 根节点的指针排名 两个参数,返回三个指针,分别指向分裂后的 三个 Treap,第一个 Treap 所有节点的排名小于给出排名,第二个 Treap 所有节点的排名等于给出排名(只可能有一个节点或没有节点),第三个 Treap 所有节点的排名大于给出排名

对于要分裂的排名 (rk),大致分裂思路如下:

  1. 首先注意到当前节点的排名必大于等于 左子树的大小,因此先比较左子树大小与当前排名
  2. 如果左子树大小大于等于当前排名,说明根节点及其右子树上所有节点的排名均大于等于当前排名,而此时根节点的左子树上仍可能有节点的排名大于当前排名,因此递归分裂左子树 (结果记为 l, mid, r),再将当前根节点的左孩子指向 r,最终返回 l, mid 与当前根节点的指针
  3. 如果左子树大小小于当前排名,注意到当前节点右子树上节点的排名必大于等于 左子树大小+当前节点出现次数,因此再次比较 左子树大小+当前节点出现次数 与 当前排名
  4. 如果 左子树大小+当前节点出现次数大于等于当前排名,又因为左子树大小大于当前排名,因此当前节点即为要找的节点直接返回 当前节点左子树的指针,当前节点指针与当前节点右子树的指针
  5. 如果 左子树大小+当前节点出现次数小于当前排名,说明根节点及其左子树上所有节点的排名均小于当前排名,而此时根节点的右子树上仍可能有节点的排名小于当前排名,因此递归分裂右子树(注意更新目标权值!) (结果记为 l, mid, r),再将当前根节点的右孩子指向 l,最终返回当前根节点的指针与 mid, r

于是可以得到按排名分裂的代码:

tuple<Node *, Node *, Node *> split_by_rk(Node *root, int rk) {    // 根节点指针,排名
	if (root == nullptr) {    // 当前节点不存在
		return {nullptr, nullptr, nullptr};
	}

	int ls_siz;    // 左子树的大小

	if (root->ch[0] == nullptr) {    // 当前节点没有左子树
		ls_siz = 0;
	} else {
		ls_siz = root->ch[0]->siz;
	}

	if (rk <= ls_siz) {    // 左子树的大小 大于等于 当前排名
		Node *l, *mid, *r;
		tie(l, mid, r) = split_by_rk(root->ch[0], rk);    // 递归分裂左子树
		root->ch[0] = r;
		root->upd_siz();    // 别忘了更新子树大小
		return {l, mid, root};
	} else if (rk <= ls_siz + root->rep_cnt) {    // 左子树的大小+当前节点的出现次数 大于等于 当前排名
		Node *lt = root->ch[0];
		Node *rt = root->ch[1];
		root->ch[0] = root->ch[1] = nullptr;
		return {lt, root, rt};    // 直接返回
	} else {    // 左子树的大小+当前节点的出现次数 小于 当前排名
		Node *l, *mid, *r;
		tie(l, mid, r) = split_by_rk(root->ch[1], rk - ls_siz - root->rep_cnt);   // 注意更新目标权值!
		root->ch[1] = l;
		root->upd_siz();    // 别忘了更新子树大小
		return {root, mid, r};
	}
}

4. 合并


合并过程接收 左Treap根节点的指针 及 右Treap根节点的指针,满足左Treap所有节点的值 小于等于 右Treap所有节点的值,返回合并后的 Treap 根节点的指针

由于合并过程需要注意 优先级满足堆的性质,因此主要考虑两根节点间优先级的关系

对于要合并的根节点 (u, v),大致合并思路如下(小根堆):

  1. 首先判断 u, v 是否存在,若都不存在返回 nullptr,若只有一个存在则返回存在的根节点的指针
  2. 如果 u的优先级 小于 v的优先级,则将 u 作为新Treap的根节点,递归合并 u右子树 与 v,并将 u右子树 指向 合并结果
  3. 如果 u的优先级 大于等于 v的优先级,则将 v 作为新Treap的根节点,递归合并 v左子树 与 u,并将 v左子树 指向 合并结果

于是可以得到合并的代码:

Node *merge(Node *u, Node *v) {    // 左Treap根节点的指针,右Treap根节点的指针
	if (u == nullptr && v == nullptr) {    // 都不存在
		return nullptr;
	}

	if (u != nullptr && v == nullptr) {    // 只有v存在
		return u;
	}

	if (v != nullptr && u == nullptr) {    // 只有u的存在
		return v;
	}

	if (u->rank < v->rank) {    // u的优先级 小于 v的优先级
		u->ch[1] = merge(u->ch[1], v);    // 递归合并
		u->upd_siz();    // 别忘了更新大小
		return u;
	} else {    // u的优先级 大于等于 v的优先级
		v->ch[0] = merge(u, v->ch[0]);    // 递归合并
		v->upd_siz();    // 别忘了更新大小
		return v;
	}
}

5. 插入节点


我们知道,由分裂操作的性质,值小于等于 val 的节点会被分到第一个 Treap

由这一性质,大致插入思路如下:

  1. 按照 val 分裂当前 Treap,则会产生 T1, T2 两棵 Treap,满足 T1 \leq val, T2> val
  2. 按照 val-1 分裂 T1,则会再次产生 T1_{left}, T1_{right} 两颗 Treap,满足 T1_{left}\leq val-1, val-1< T1_{right}\leq val
  3. 此时若 T1_{right} 中含有节点,则其权值必等于 val,将其 出现次数+1 即可
  4. 若 T1_{right} 中不含有节点,则创建一个权值为 val 的新节点
  5. 最后按顺序将所有分裂出的 Treap 合并回去即可

于是可以得到插入节点的代码:

void insert(int val) {    // 要插入节点的权值
    auto temp = split(root, val);    // 按照 val 分裂
	auto l_tr = split(temp.first, val - 1);    // 再次按照 val-1 分裂
	Node *new_node;

	if (l_tr.second == nullptr) {    // 原 Treap 中没有权值为 val 的节点
		new_node = new Node(val);    // 创建权值为 val 的新节点
	} else {    // 原 Treap 中有权值为 val 的节点
		l_tr.second->rep_cnt++;
		l_tr.second->upd_siz();    // 别忘了更新子树大小
	}

	Node *l_tr_c;

	if (l_tr.second == nullptr) {
		l_tr_c = merge(l_tr.first, new_node);
	} else {
		l_tr_c = merge(l_tr.first, l_tr.second);
	}

	root = merge(l_tr_c, temp.second);    // 最后合并回来 (root 为整个 Treap 的根节点指针)
}

6. 删除节点


删除节点使用与插入节点相似的方法,具体参见代码注释

void del(int val) {    // 要删除节点的权值
	auto temp = split(root, val);    // 按照 val 分裂
	auto l_tr = split(temp.first, val - 1);    // 再次按照 val-1 分裂

	if (l_tr.second->rep_cnt > 1) {    // 如果要删除节点的出现次数大于1
		l_tr.second->rep_cnt--;    // 将出现次数减去1
		l_tr.second->upd_siz();    // 别忘了更新子树大小
		l_tr.first = merge(l_tr.first, l_tr.second);    // 最后合并回来
	} else {
		if (temp.first == l_tr.second) {    // 如果 temp 的左子树只有 要删除节点 这一个节点
			temp.first = nullptr;    // 标记左子树为 nullptr 表示删除
		}

		delete l_tr.second;    // 删除指针
		l_tr.second = nullptr;
	}

	root = merge(l_tr.first, temp.second);    // 最后合并回来
}

7. 根据值查询排名


根据 val-1 分裂 Treap,分裂后的第一个 Treap 的大小即为所求

int query_rank_by_val(Node *root, int val) {    // 当前根节点,要查询排名的权值
	auto temp = split(root, val - 1);    // 按照 val-1 分裂

	int res;

	if (temp.first == nullptr) {    // 没有比 val 小的节点
		res = 1;
	} else {
		res = temp.first->siz + 1;    // 返回第一个 Treap 的大小加上1 (参见排名的定义)
	}

	root = merge(temp.first, temp.second);    // 最后合并回去
	return res;
}

8. 根据排名查询值


根据 rk 分裂 Treap (调用 split\_by\_rk() 函数),第二个 Treap 的值即为所求

int query_val_by_rank(Node *root, int rk) {    // 当前根节点,要查询权值的排名
	Node *l, *mid, *r;
	tie(l, mid, r) = split_by_rk(root, rk);    // 按照 rk 分裂
		
	if (mid == nullptr){    // 没有排名为 rk 的节点 (rk 大于整个 Treap 的大小)
		return 2147483647;
	}

	int res = mid->val;    // 返回第二个 Treap 的值
	root = merge(merge(l, mid), r);    // 最后合并回去
	return res;
}

9. 查询给定值的前驱


前驱定义为,小于 val 但最大的数

问题相当于在比 val 小的所有节点中找出排名最大的节点

大致查前驱思路:

  1. 按照 val-1 分裂 Treap
  2. 在分裂后的第一个 Treap 中寻找 rk 为第一个 Treap 的大小的节点,其值即为所求

于是可以得到查前驱的代码:

int query_pre(int val) {    // 要查前驱的权值
	auto temp = split(root, val - 1);    // 按照 val-1 分裂
		
	if (temp.first == nullptr){    // 前驱不存在
		return -2147483647;
	}

	int res = query_val_by_rank(temp.first, temp.first->siz);
	root = merge(temp.first, temp.second);    // 最后合并回去
	return res;
}

10. 查询给定值的后继


后继定义为,大于 val 但最小的数

与查前驱类似,问题相当于在比 val 大的所有节点中找出排名最小的节点 (其实就是为 1)

大致查后继思路:

  1. 按照 val 分裂 Treap
  2. 在分裂后的第二个 Treap 中寻找 rk 为 1 的节点,其值即为所求

于是可以得到查后继的代码:

int query_nxt(int val) {    // 要查后继的权值
	auto temp = split(root, val);    // 按照 val 分裂
		
	if (temp.second == nullptr){    // 后继不存在
		return 2147483647;
	}

	int res = query_val_by_rank(temp.second, 1);
	root = merge(temp.first, temp.second);    // 最后合并回来
	return res;
}

11. 练习题目


相信你已经掌握 FHQ-Treap 的基本操作了,那么做道板子题练练手吧~

P3369【模板】普通平衡树


另附上此题的 AC 代码,作为本篇笔记的代码汇总

#include <bits/stdc++.h>
using namespace std;

struct Node {
	Node *ch[2];
	int val;
	int rank;
	int rep_cnt;
	int siz;

	Node (int _val) : val(_val), rep_cnt(1), siz(1) {
		ch[0] = ch[1] = nullptr;
		rank = rand();
	}

	void upd_siz() {
		siz = rep_cnt;

		if (ch[0] != nullptr) {
			siz = siz + ch[0]->siz;
		}

		if (ch[1] != nullptr) {
			siz = siz + ch[1]->siz;
		}
	}
};

struct Nrot_Treap {
	Node *root;

	pair<Node *, Node *> split(Node *root, int key) {
		if (root == nullptr) {
			return {nullptr, nullptr};
		}

		if (root->val <= key) {
			auto temp = split(root->ch[1], key);
			root->ch[1] = temp.first;
			root->upd_siz();
			return {root, temp.second};
		} else {
			auto temp = split(root->ch[0], key);
			root->ch[0] = temp.second;
			root->upd_siz();
			return {temp.first, root};
		}
	}

	tuple<Node *, Node *, Node *> split_by_rk(Node *root, int rk) {
		if (root == nullptr) {
			return {nullptr, nullptr, nullptr};
		}

		int ls_siz;

		if (root->ch[0] == nullptr) {
			ls_siz = 0;
		} else {
			ls_siz = root->ch[0]->siz;
		}

		if (rk <= ls_siz) {
			Node *l, *mid, *r;
			tie(l, mid, r) = split_by_rk(root->ch[0], rk);
			root->ch[0] = r;
			root->upd_siz();
			return {l, mid, root};
		} else if (rk <= ls_siz + root->rep_cnt) {
			Node *lt = root->ch[0];
			Node *rt = root->ch[1];
			root->ch[0] = root->ch[1] = nullptr;
			return {lt, root, rt};
		} else {
			Node *l, *mid, *r;
			tie(l, mid, r) = split_by_rk(root->ch[1], rk - ls_siz - root->rep_cnt);
			root->ch[1] = l;
			root->upd_siz();
			return {root, mid, r};
		}
	}

	Node *merge(Node *u, Node *v) {
		if (u == nullptr && v == nullptr) {
			return nullptr;
		}

		if (u != nullptr && v == nullptr) {
			return u;
		}

		if (v != nullptr && u == nullptr) {
			return v;
		}

		if (u->rank < v->rank) {
			u->ch[1] = merge(u->ch[1], v);
			u->upd_siz();
			return u;
		} else {
			v->ch[0] = merge(u, v->ch[0]);
			v->upd_siz();
			return v;
		}
	}

	void insert(int val) {
		auto temp = split(root, val);
		auto l_tr = split(temp.first, val - 1);
		Node *new_node;

		if (l_tr.second == nullptr) {
			new_node = new Node(val);
		} else {
			l_tr.second->rep_cnt++;
			l_tr.second->upd_siz();
		}

		Node *l_tr_c;

		if (l_tr.second == nullptr) {
			l_tr_c = merge(l_tr.first, new_node);
		} else {
			l_tr_c = merge(l_tr.first, l_tr.second);
		}

		root = merge(l_tr_c, temp.second);
	}

	void del(int val) {
		auto temp = split(root, val);
		auto l_tr = split(temp.first, val - 1);

		if (l_tr.second->rep_cnt > 1) {
			l_tr.second->rep_cnt--;
			l_tr.second->upd_siz();
			l_tr.first = merge(l_tr.first, l_tr.second);
		} else {
			if (temp.first == l_tr.second) {
				temp.first = nullptr;
			}

			delete l_tr.second;
			l_tr.second = nullptr;
		}

		root = merge(l_tr.first, temp.second);
	}

	int query_rank_by_val(Node *root, int val) {
		auto temp = split(root, val - 1);

		int res;

		if (temp.first == nullptr) {
			res = 1;
		} else {
			res = temp.first->siz + 1;
		}

		root = merge(temp.first, temp.second);
		return res;
	}

	int query_val_by_rank(Node *root, int rk) {
		Node *l, *mid, *r;
		tie(l, mid, r) = split_by_rk(root, rk);
		
		// debug
		if (mid == nullptr){
			return 2147483647;
		}

		int res = mid->val;
		root = merge(merge(l, mid), r);
		return res;
	}

	int query_pre(int val) {
		auto temp = split(root, val - 1);
		
		// debug
		if (temp.first == nullptr){
			return -2147483647;
		}

		int res = query_val_by_rank(temp.first, temp.first->siz);
		root = merge(temp.first, temp.second);
		return res;
	}

	int query_nxt(int val) {
		auto temp = split(root, val);
		
		// debug
		if (temp.second == nullptr){
			return 2147483647;
		}

		int res = query_val_by_rank(temp.second, 1);
		root = merge(temp.first, temp.second);
		return res;
	}
};

int N;
Nrot_Treap tr;

int main() {
	srand(time(0));
	scanf("%d", &N);

	for (int i = 1; i <= N; i++) {
		int op, data;
		scanf("%d%d", &op, &data);

		if (op == 1) {
			tr.insert(data);
		} else if (op == 2) {
			tr.del(data);
		} else if (op == 3) {
			printf("%d\n", tr.query_rank_by_val(tr.root, data));
		} else if (op == 4) {
			printf("%d\n", tr.query_val_by_rank(tr.root, data));
		} else if (op == 5) {
			printf("%d\n", tr.query_pre(data));
		} else if (op == 6) {
			printf("%d\n", tr.query_nxt(data));
		}
	}

	return 0;
}

创作不易,各位看官若觉得有用就点个赞吧~

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
可持久化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 ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值