FHQ-Treap(非旋 Treap) 学习笔记

参考文章

FHQ-Treap 学习笔记

所以我这个学习笔记。。就是抄一遍?因为他讲的太清楚了!

那这篇学习笔记存在的意义是?学习笔记嘛,一方面也是写给自己看的。。

Treap

Treap = BST + Heap,什么意思?

一开始每个节点都具有一个权值 val,现在我们给每个节点都增加一个修正值 key,使得 Treap 在关注 val 时是 BST(二叉搜索树),在关注 key 时是 Heap(堆),这个堆需要满足每个节点严格小于所有子树里的儿子(也就是说这里是小根堆)。可以证明,给定一些点的集合,构成的 Treap 只可能有一个,且当 key 随机时树高期望 O ( log ⁡ n ) O(\log n) O(logn)

FHQ-Treap 的基本操作

FHQ-Treap 给出了一种不基于旋转的维护方法,其本质是依靠 Treap 的结构唯一性。

FHQ-Treap 我吹爆,实在是太好写了

分裂

有两种分裂方式,看具体要做什么问题。

按权值分裂

我们要做的事:给定一个 Treap,将其中所有节点按照是否小于等于 queryval 分类,分别分裂进两个不同的 Treap。

借用参考文章的一张图。。

我们动态维护两棵要造的 Treap 的虚点 x, y(这两个虚点也能表示这两棵 Treap 本身,故下文不会区分了) 和原 Treap 上的点 now,通过判断 val[now] 与 queryval 的大小来判断左右子树的归属,并递归到儿子中继续分裂。

比如如果 val[now]<=queryval,那么{左子树+now}(也就是第2步中蓝色部分)都将归属 x,将这一部分拷过去后虚点 x 前往右儿子,now 也前往右儿子。

实现:

void split(int now, int k, int& x, int& y) { // 注意 x,y 是传址
	if (!now) { x = y = 0; return; }
	if (val[now] <= k) x = now, split(rs[now], k, rs[now], y); // x = now 是拷贝过去,递归进入下一层继续分裂
	else y = now, split(ls[now], k, x, ls[now]); // {右子树+now} 被拷贝到 y
	pushUp(now); // 上传信息,在下面【按排名分裂】会提到
}

按排名分裂

这里的排名指的是中序遍历(左中右)的排名。

功能:将排名 <= k 的放到 x 里,>k 的放到 y 里。

我们只需要算出 now 的 “val” 是多少,如果是按排名分裂,这个 “val” 就是 siz[ls[now]]+1 了。

那么首先我们需要维护每个点的子树大小 siz,这可以通过上传标记完成:

void pushUp(int x) { siz[x] = siz[ls[x]] + siz[rs[x]] + 1; }

接下来在写的时候我们需要关注什么时候加 pushUp,比如按排名分裂的实现:

void split(int now, int k, int& x, int& y) {
	if (!now) { x = y = 0; return; }
	if (siz[ls[now]] + 1 <= k) x = now, split(rs[now], k - siz[ls[now]] - 1, rs[now], y);
	else y = now, split(ls[now], k, x, ls[now]);
	pushUp(now);
}

合并

给两个 Treap 我们就合并这行不通的,但我们可以做的是:给定两个 Treap x, y 并保证 y 的所有点的 val 都大于等于 x 所有点的 val,将它们合并成一个 Treap。可以发现这个功能是随分裂而生的,因为分裂出来的两个 Treap 正好满足上述条件。

还是厚颜无耻地借用一下参考文章里的图。。

如图,我们沿用了上文分裂的例子,黑底白字是 val 权值,而白底黑字是 key 修正值。当一个节点被染蓝,说明此节点已被放入最终获得了主树内。(摘自参考文章)

我们要同时维护 val 的二叉树性质和 rnd 的小根堆性质。令 merge(x,y) (两个参数 x,y 既可以被理解为点,也可以被理解为它作为根所代表的 Treap) 表示将 x,y 两棵 Treap 合并,返回合并后的根。可以确定的是根是 x,y 中的其中一个,所以我们比较 key 来判断谁是根。

int merge(int x, int y) { 
	if (!x || !y) return x + y;
	if (key[x] < key[y]) { rs[x] = merge(rs[x], y), pushUp(x); return x; } // 根是 x,y 肯定接在 rs[x] 里面。
	else { ls[y] = merge(x, ls[y]), pushUp(y); return y; } // 根是 y,x 肯定接在 ls[y] 里面。
}

那么 FHQ-Treap 最重要的两个操作就讲完了。

FHQ-Treap 维护集合操作 - 引例 [模板]普通平衡树

题意

链接

插入

我们将 Treap 分裂成 <=x 和 >x 的,然后新建一个权值为 x 的节点(也就是一棵大小为 1 的 Treap),将三者按序合并即可。

void insert(int x) { r1 = r2 = 0, split(root, x, r1, r2), root = merge(merge(r1, newnode(x)), r2); }

删除

我们将 Treap 分裂成三部分:<x,=x,>x,然后在 =x 中把根去掉(也就是把根的左右子合并),再将三部分合并即可。

void remove(int x) {
	r1 = r2 = r3 = 0, split(root, x, r1, r2), split(r1, x - 1, r1, r3); //r1(<x),r3(=x),r2(>x)
	r3 = merge(ls[r3], rs[r3]), root = merge(merge(r1, r3), r2);
}

求排名

我们将 Treap 分裂成 <x 和 >=x,直接查 <x 的大小就可。

int rnk(int x) {
	int ret = 0; r1 = r2 = 0, split(root, x - 1, r1, r2), ret = siz[r1] + 1, root = merge(r1, r2);
	return ret;
}

求第 k 大

按照正常的 BST 搜索即可。

int kth(int x, int k) {
	if (k <= siz[ls[x]]) return kth(ls[x], k);
	else if (k > siz[ls[x]] + 1) return kth(rs[x], k - siz[ls[x]] - 1);
	else return x;
}

求前驱 / 后继

前驱:将 Treap 分为两部分:<x,>=x,找到 <x 中最大的(也就是从根开始一直往右走)即可。

后继:同理。

int pre(int x) {
	split(root, x - 1, r1, r2); int p;
	for (p = r1; rs[p]; p = rs[p]);
	root = merge(r1, r2); return p;
}
int nex(int x) {
	split(root, x, r1, r2); int p;
	for (p = r2; ls[p]; p = ls[p]);
	root = merge(r1, r2); return p;
}

完整代码

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

typedef long long ll;
const int MAXN = 100005;

int N, root, ls[MAXN], rs[MAXN], val[MAXN], key[MAXN], siz[MAXN], Tlen;
int newnode(int x) { ++Tlen, val[Tlen] = x, key[Tlen] = rand(), siz[Tlen] = 1; return Tlen; }
void pushUp(int x) { siz[x] = siz[ls[x]] + siz[rs[x]] + 1; }
void split(int now, int k, int& x, int& y) {
	if (!now) { x = y = 0; return; }
	if (val[now] <= k) x = now, split(rs[now], k, rs[now], y);
	else y = now, split(ls[now], k, x, ls[now]);
	pushUp(now);
}
int merge(int x, int y) {
	if (!x || !y) return x + y;
	if (key[x] > key[y]) { rs[x] = merge(rs[x], y), pushUp(x); return x; }
	else { ls[y] = merge(x, ls[y]), pushUp(y); return y; }
}
int r1, r2, r3;
void insert(int x) { r1 = r2 = 0, split(root, x, r1, r2), root = merge(merge(r1, newnode(x)), r2); }
void remove(int x) {
	r1 = r2 = r3 = 0, split(root, x, r1, r2), split(r1, x - 1, r1, r3), r3 = merge(ls[r3], rs[r3]), root = merge(merge(r1, r3), r2);
}
int rnk(int x) {
	int ret = 0; r1 = r2 = 0, split(root, x - 1, r1, r2), ret = siz[r1] + 1, root = merge(r1, r2);
	return ret;
}
int kth(int x, int k) {
	if (k <= siz[ls[x]]) return kth(ls[x], k);
	else if (k > siz[ls[x]] + 1) return kth(rs[x], k - siz[ls[x]] - 1);
	else return x;
}
int pre(int x) {
	split(root, x - 1, r1, r2); int p;
	for (p = r1; rs[p]; p = rs[p]);
	root = merge(r1, r2); return p;
}
int nex(int x) {
	split(root, x, r1, r2); int p;
	for (p = r2; ls[p]; p = ls[p]);
	root = merge(r1, r2); return p;
}

int main() {
	scanf("%d", &N); int op, x;
	srand(19260817);
	for (int i = 1; i <= N; ++i) {
		scanf("%d%d", &op, &x);
		if (op == 1) insert(x);
		else if (op == 2) remove(x);
		else if (op == 3) printf("%d\n", rnk(x));
		else if (op == 4) printf("%d\n", val[kth(root, x)]);
		else if (op == 5) printf("%d\n", val[pre(x)]);
		else if (op == 6) printf("%d\n", val[nex(x)]);
	}
	return 0;
}

FHQ-Treap 维护区间操作 - 引例 [模板]文艺平衡树

题意

链接

怎么维护

我们令 Treap 表示的序列是它的中序遍历所构成的序列。

如果我们要求区间和,我们只需要将 Treap 按排名把 l<=排名<=r 这一部分分出来,然后求区间和(在每个点上维护区间和的值然后 pushup)

如果我们要输出整个序列,我们只需要跑中序遍历。

如果我们要插入,我们只需要用上面【维护集合操作】中的插入方法来插入。

懒惰标记

但这题的翻转操作怎么维护?使用懒惰标记。

但这已经在线段树给讲透了?所以好像也没啥可讲的。

完整代码

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

const int MAXN = 100005;
int N, M, root, ls[MAXN], rs[MAXN], val[MAXN], key[MAXN], siz[MAXN], tag[MAXN], Tlen;
int newnode(int x) { ++Tlen, val[Tlen] = x, key[Tlen] = rand(), siz[Tlen] = 1; return Tlen; }
void pushUp(int x) { siz[x] = siz[ls[x]] + siz[rs[x]] + 1; }
void cover(int x) { tag[x] ^= 1, swap(ls[x], rs[x]); }
void pushDown(int x) { if (tag[x]) cover(ls[x]), cover(rs[x]), tag[x] = 0; }
void split(int now, int k, int& x, int& y) {
	if (!now) { x = y = 0; return; }
	pushDown(now);
	if (siz[ls[now]] + 1 <= k) x = now, split(rs[now], k - siz[ls[now]] - 1, rs[now], y);
	else y = now, split(ls[now], k, x, ls[now]);
	pushUp(now);
}
int merge(int x, int y) {
	if (!x || !y) return x + y;
	pushDown(x), pushDown(y);
	if (key[x] < key[y]) { ls[y] = merge(x, ls[y]), pushUp(y); return y; }
	else { rs[x] = merge(rs[x], y), pushUp(x); return x; }
}
void modify(int l, int r) {
	int r1 = 0, r2 = 0, r3 = 0;
	split(root, r, r1, r2), split(r1, l - 1, r1, r3), cover(r3);
	root = merge(merge(r1, r3), r2);
}
void dfs(int x) {
	pushDown(x);
	if (ls[x]) dfs(ls[x]);
	printf("%d ", val[x]);
	if (rs[x]) dfs(rs[x]);
}

int main() {
	scanf("%d%d", &N, &M); int l, r;
	srand(19260817);
	for (int i = 1; i <= N; ++i) root = merge(root, newnode(i));
	while (M--) {
		scanf("%d%d", &l, &r), modify(l, r);
	}
	dfs(root);
	return 0;
}

例题

[BJOI2017] 喷施水战改

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可持久化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 ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值