【老鼠看不懂的数据结构】FHQTreap 初识

Treap 弱平衡的随机性很强的老鼠看不懂的平衡树

Q:为什么叫 Treap?
A:看看二叉搜索树(BST)和堆(Heap),组合起来就是 Treap

其中,二叉搜索树的性质是:
左子节点的值 (val) 比父节点小
右子节点的值 (val) 比父节点大

如果这些节点的值都一样,这棵树就会退化成一颗(?)链。

对, 我知道你在想什么——并查集。
虽然都会被傻老鼠乱搞退化成链,但优化方式大有不同。

优化 - Priority - 玄学抽卡

笨蛋老鼠可能还有疑惑,为什么链是naive的而树是超棒的呢?
从OIwiki偷两张图来解释:
看,这是一颗正常的树,基本上可以看作满二叉树,一次查询只需要 O(log2n)的时间复杂度
正常的树
这是一颗(?)被老鼠乱搞以后形成的链,一次查询的时间复杂度劣化到了 O(n)
链
那么,既然屑老鼠已经说了这棵树有堆的性质,所以就要给每一个结点上一个随机的优先级Priority
让它同时成为一个堆,在双重特征下保持完全二叉树的形状

PS:傻老鼠脑子糊涂了,首先树得满足BST的性质,然后空下来时维护Heap的性质

大功告成,现在该知道节点里面应该放什么了。

node节点(为什么不用类封装?因为老鼠不会)
struct node{
	node *child[2];
	int val,ranf,cnt,siz;
	node(int __val) : val(__val), cnt(1), siz(1){
		child[0] = child[1] = nullptr;//左右儿子初始化 
		ranf = rand();//玄学抽卡 
	} 
	node(node *__node){
		val = __node->val, ranf = __node->ranf, cnt = __node->cnt, siz = __node->siz;
	}
	void ud_siz(){
		siz = cnt;
		if(child[0] != nullptr)siz += child[0]->siz;
		if(child[1] != nullptr)siz += child[1]->siz; 
	}
};

Treap的重要操作 Split & Merge

分裂过程接受两个参数:根指针 cur、关键值 key。结果为将根指针指向的 treap 分裂为两个 treap,第一个 treap 所有结点的值val小于等于 key,第二个 treap 所有结点的值大于 key
该过程首先判断 key 是否小于 cur 的值,若小于,则说明 cur 及其右子树全部大于 key,属于第二个 treap。当然,也可能有一部分的左子树的值大于 key,所以还需要继续向左子树递归地分裂。对于大于 key 的那部分左子树,我们把它作为 cur 的左子树,这样,整个 cur 上的节点都是大于 key 的。
相应的,如果 key 大于等于 cur 的值,说明 cur 的整个左子树以及其自身都小于 key,属于分裂后的第一个 treap。并且,cur 的部分右子树也可能有部分小于 key,因此我们需要继续递归地分裂右子树。把小于 key 的那部分作为 cur 的右子树,这样,整个 cur 上的节点都小于 key
下图展示了 cur 的值小于等于 key 时按值分裂的情况. ——OIWIKI

老鼠才不会解释,因为老鼠没脑子。

Split
pair<node *, node *> split(node *rt,int key){
	if(rt == nullptr)return {nullptr, nullptr};
	if(rt->val <= key){
	auto temp = split(rt->child[1],key);
		rt->child[1] = temp.first;
		rt->ud_siz();
		return {rt,temp.second};
	}
	else{
		auto temp = split(rt->child[0],key);
		rt->child[0] = temp.second;
		rt->ud_siz();
		return {temp.first,rt};
	}
	}

合并过程接受两个参数:左 treap 的根指针 u、右 treap 的根指针 v。必须满足 u 中所有结点的值小于等于 v 中所有结点的值。一般来说,我们合并的两个 treap 都是原来从一个 treap 中分裂出去的,所以不难满足 u 中所有节点的值都小于 v
在旋转 treap 中,我们借助旋转操作来维护 priority 符合堆的性质,同时旋转时还不能改变树的性质。在无旋 treap 中,我们用合并达到相同的效果。
因为两个 treap 已经有序,所以我们在合并的时候只需要考虑把哪个树「放在上面」,把哪个「放在下面」,也就是是需要判断将哪个一个树作为子树。显然,根据堆的性质,我们需要把 priority 小的放在上面(这里采用小根堆)。
同时,我们还需要满足搜索树的性质,所以若 u 的根结点的 priority 小于 v 的,那么 u 即为新根结点,并且 v 因为值比 u 更大,应与 u 的右子树合并;反之,则 v 作为新根结点,然后因为 u 的值比 v 小,与 v 的左子树合并。——OIWIKI
老鼠懒,自己看。

Merge
node *merge(node *u,node *v){
		if(u == nullptr && v == nullptr)return nullptr;
		if(u != nullptr && v == nullptr)return u;
		if(u == nullptr && v != nullptr)return v;
		if(u->ranf < v->ranf){
			u->child[1] = merge(u->child[1],v);
			u->ud_siz();
			return u;
		}
		else{
			v->child[0] = merge(u, v->child[0]);
			v->ud_siz();
			return v;
		}
	}

不好用,才不用——⑨老鼠

作为笨蛋老鼠,能看懂这个东西怎么用才奇怪吧,只能分裂和合并的数据结构,鬼才用嘞!
笨蛋老鼠!这个东西可以整很多活的

查询小于等于val的数的个数:考察根节点的值和val,如果val小于根节点,递归进左子树,否则递归进右子树。
插入val:先查询Rank(val),然后按照rank把整个TreapSplit成两个,把val做成一个新节点,Merge到里面。
删除val:先查询Rank(val),然后按照rank把整个TreapSplit成三个,删除需要的点,最后Merge剩下两个。
查询第K个值:把整个TreapSplit成三个,输出需要的值,最后合并起来。

好啊,你的区间呢?

夸下的海口终究会被打qwq。
发现了没,这些个操作全是区间的,想想你的线段树怎么区间修改优化的?
LazyTag救我狗命对吧!
所以,直接打标记建线段树到处乱搞,怎么在树链剖分上整的活大多数都能整!

posted @   咩有脑子吱嘎鼠  阅读(48)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示