FHQ-Treap 学习笔记

平衡树 FHQ-Treap:


一, 一些前言:

虽记吾几时学平衡树,知平衡树甚多,还记正值 Splay 时期,代码二百余行,好不威风!可其难于调也,一调兮为一日,日复一日,未曾调出郁闷的出纳员,而吾早已郁闷。

待三日已过,遂 A 此题,余感概道 Splay 难作,乃学 Treap 矣。

平衡树多种,FHQ-Treap 可谓最强平衡树也,何处此言?待会知之矣。

好了不扯了,也不想编了。


二, FHQ-Treap 的概述:

毕竟是最强平衡树,它可以维护下标,也可以维护值域,还能维护区间修改,还能可持久化!这可是隔壁厕所里的 Splay 干不了的事!

但是嘛,没有东西是十全十美的,它总有比隔壁厕所里的 Splay 弱的地方。

是什么呢?

在维护 LCT 上!

隔壁厕所里的 Splay 维护 LCT 只需要 nlogn 的复杂度,而 FHQ-Treap 有 nlog2n 的复杂度,大了一点,但始终比隔壁厕所里的 Splay 写起来好太多了。

Splay:我厕所上完了哥。

啥,你说你不知道 LCT 是个啥?

其实我也没学 LCT。

那你待会可以去看看 LCT 的详解。

like this.

好的我们继续。

相较于有旋 Treap 而言,FHQ-Treap 最典型的特点即是不需要旋转操作,因此 FHQ-Treap 也称无旋 Treap,无旋 Treap 的操作方式使得它天生支持维护序列,可持久化等特性。

FHQ-Treap 仅有两种核心操作,分裂 与 合并,通过这两种神一般的操作,在很多情况下可以比有旋 Treap 方便许多,下面逐一介绍这两种操作。


三, FHQ-Treap 的分裂操作:

分裂 - Split 操作,简单来说,就是把一个大 Treap 分成两个小 Treap,且保证第一个 Treap 的节点在第二个前面。

Split 操作有两种实现方式,都很有用。

  1. 按值分裂:

何为按值分裂?就是把权值小于 x 的节点分在第一个 Treap 里,其余在第二个 Treap 里。

怎么分裂?如图所示:

就是酱紫~

代码实现:

il void split(int p, int v, int &l, int &r){//p 是当前 Treap 的根
	//l,r 分别是 FHQ-Treap 分裂后得到的两个 FHQ-Treap 的根,这里用了引用的方式,便于传递值
	if (!p){//空树
		l = r = 0;
		return ;
	}
	if (t[p].v <= v){
		//当前节点的值小于等于 x(即我写的 v),则证明该节点的左子树上所有节点的值也都小于等于 x			
		l = p;//让该节点成为左树的节点
		split(t[l].r, v, t[l].r, r);//继续递归分裂当前节点的右子树
	}
	else{
		//当前节点的值大于 x,则证明该节点的左子树上所有节点的值也都大于 x
		r = p;//让该节点成为右左树的节点
		split(t[r].l, v, l, t[r].l);//继续递归分裂当前节点的左子树
	}
	up(p);//一定要记着 pushup,因为 p 点的左右子树大小变化了,即总个数变化了,需要更新,否则会导致节点个数的信息不对
}

  1. 按排名分裂:

何又为按排名分裂?相信你已经猜到了,就是把前 x 个节点分在第一个 Treap 里,其余在第二个 Treap 里。

思路和按值分裂的思路是一样的,就不说了,看看如何实现:

il void split(int p, int v, int &l, int &r){
	if (!p){//空树
		l = r = 0;
		return ;
	}
	int ln = t[t[p].l].sn;//左子树的节点个数
	if (ln < v){//左边不足 k(即我写的 v)个,递归从右边再取出剩余部分,补给第一个 Treap
		l = p;
		split(t[p].r, v - ln - 1, t[l].r, r);
	}
	else{//同上,有 k 个就要分到第二个 Treap 里
		r = p;
		split(t[p].l, v, l, t[r].l);
	}
	up(p);
}//十分简单

切记,排名分裂也要牢牢掌握。

分裂讲完了,撒花!


四, FHQ-Treap 的合并操作:

我想起了半年以前自学的并查集()

诶,第二个 merge,难度咋样?

u1s1,在我看来,跟第一个 merge 难度差不多,就稍微难那么一些而已,真的。

合并 - Merge 操作,简单来说,就是把一个小 Treap 和另一个小 Treap 合并成一个 大 Treap,且保证前一个 Treap 的遍历顺序都在后一个前面。(说白了就是保持 BST 结构不被破坏)

如上文所说,虽然合并操作看起来很简单,但也不能乱合并,要必须保持 BST 的结构,否则你还叫啥 Treap

要合并两棵 Treap,当且仅当左 Treap 上的所有节点的权值全都小于等于右 Treap 上节点的最小权值,或两棵树有空树。

这是个重要的性质。

合并时,由于是我们设置的随机值 rnd 来保持的 Treap 的 BST 结构,则根据左右两树 rnd 的值的大小进行合并操作。

详见代码:

il int merge(int l, int r){//左右树的根节点
	if (!l || !r) return l | r;//有空树,返回非空树 l+r
	//如果左树上当前节点的 rnd 值小于右树上当前节点的 rnd 值
	if (t[l].rnd < t[r].rnd){
		//将 l 变为合并后的新 FHQ-Treap 的根节点。
		t[l].r = merge(t[l].r, r);//继续递归确定 l 的右子树该接谁
		up(l);//一定记得要上传
		return l;//返回新得到的根节点
	}
	else{//同上
		t[r].l = merge(l, t[r].l);
		up(r);
		return r;
	}
}//十分简单

合并也讲完了,撒花撒花!!


五, FHQ-Treap 的其他杂用操作实现

一个个看吧。

  1. insert

插入一个权值为 v 的点。

把 Treap 按照 v 的值分裂,插入后再按照顺序合并回去。

code:

il void ins(int v){
	split(root, v - 1, tl, tr);
	//create(v) 是新建一个权值为 v 的节点
	root = merge(merge(tl, create(v)), tr);
}

就这么短。


  1. delete

删除一个权值为 v 的节点。

连续两次分裂,先按值分裂出 >v1 的子树 tr,再按值分裂出 >v1v 的子树 tmp,此时 tmp 树里的节点权值全是 v,删除根合并左右子树即可。(因为只删一个 v,所以不直接删掉 tmp 这棵树,只删根节点)

code:

il void del(int v){
	int tmp;
	split(root, v - 1, tl, tr);
	split(tr, v, tmp, tr);
	//两次分裂得到全是 v 的 tmp 树
	tmp = merge(t[tmp].l, t[tmp].r);//去掉根,即不合并根
	root = merge(tl, merge(tmp, tr));
}

还是很短。


  1. Findrk

查找 v 在 Treap 中的排名。

先分裂出 <v 的一棵树 tl, 然后 v 的排名就是 tl 的大小 +1 即可。

code:

il int find_rk(int v){
	split(root, v - 1, tl, tr);
	int rk = t[tl].sn + 1;
	root = merge(tl, tr);
	return rk;
}

短到注释都不想打了。


  1. Findval

查找 Treap 中 排名为 k 的数。

这里发现用递归搜索还更简洁,所以就用递归啦,思想见 BST。

code:

il int find_val(int p, int k){
	int ln = t[t[p].l].sn + 1;
	if (ln == k) return t[p].v;
	else if (ln < k) return find_val(t[p].r, k - ln);
	else return find_val(t[p].l, k);
}

  1. Pre

求数 v 的前驱。

v 的前驱实际上就是权值 <v 的这棵树 tl
中排名最后的那个数,用上 Findval 函数即可。

code:

il int pre(int v){
	split(root, v - 1, tl, tr);
	int tmp = find_val(tl, t[tl].sn);//这里 tl 树的最后一名的排名是它的节点总个数
	root = merge(tl, tr);
	return tmp;
}

有超过 10 行的吗。


  1. Next

求数 v 的后继。

v 的后继实际上就是权值 >v 的这棵树 tr
中排名第一的那个数,用上 Findval 函数即可。

code:

il int nxt(int v){
	split(root, v, tl, tr);
	int tmp = kth_find(tr, 1);
	root = merge(tl, tr);
	return tmp;
}

看来没有超过 10 行的了。

讲完了,撒花撒花撒花!!!


六, 结语:

这就是 FHQ-Treap 的基本操作啦,区间操作什么的就先放放,还有,建议可以翻到上面去看一下 LCT

呼,总算写完了。

【Markdown 300 行祭】。

posted @   Flying-hq  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示