FHQ-Treap 学习笔记

前言

  • Super_lollipop教大家学平衡树,msjing已严肃学习qwq
  • sbmqwm:你真学会了吗
  • msjing:555我真学会了

介绍

  • 平衡树有大量的写法,代表有 \(Treap\)\(Splay\),替罪羊树等,msjing学的是无旋 \(Treap\) 其实msjing还学了平板电视
  • \(Treap\),平衡树的一种,依据 \(BST\)\(Heap\) 实现,不像有旋 \(Treap\) 一样需要依据旋转维护平衡
  • \(FHQ\)-\(Treap\),是一种无旋的 \(Treap\) 且是非常常用的,是一个单 \(\log\) 但是常数飞起来

实现及维护

  • \(FHQ\)-\(Treap\) 的实现依据堆性质和 \(BST\),先说 \(BST\)

\(BST\)

  • \(BST\),全称二叉搜索树,其左子树上所有点的附加权值均小于其根节点的值,右子树上所有点的附加权值均大于其根节点的值, 就是中序遍历是有序的
  • 那么我们发现 \(BST\) 上可以做很多东西,但是由于 \(BST\) 不保证树高,即不保证平衡,因此所有的操作都是 \(O(H)\) 的,其中 \(H\) 为树高,那么我们发现,\(BST\) 可能退化成链,就太长了
  • 所以我们利用堆来维护
  • 如何利用堆来维护呢

  • 堆就不必多说了,priority_queue 都用过吧,这就是堆了
  • 那么我们如何利用堆让树平衡呢,我们可以直接 \(rand\) 一个值给每个节点,按堆和 \(BST\) 的要求搞一下,然后就行了
  • 从oiwiki偷张图吧woc偷不了
    graph
  • sbmqwm:图都能贺啊msjing你无敌了
  • 黑色字的是 \(BST\) 的,蓝色的是堆的
    然后经过一阵 \(nb\) 的复杂的证明,可以证得树高是 \(log_n\) 的,树高保证了,我们就可以搞许多东西了

\(Split\) & \(Merge\)

  • \(Split\)\(Merge\)\(FHQ\) 重要的维护平衡的操作,也是前面说的与别的平衡树不一样的地方(由于树的结构是确定的),是全部操作的基础

\(Split\)

  • \(Split\) 操作主要是依据 \(v\) 把树分裂成小于等于 \(v\) 和大于 \(v\) 两部分,具体怎么实现呢
  • 首先我们先搞两个滚木虚空节点,利用取地址符来直接对这两个点改动
    • msjing:我真的会取地址
  • 然后依据 \(v\) 向下递归,遇到比 \(v\) 小或等的就直接把这个点和它的左子树全塞到左边的虚空节点上,左边的虚空节点改为这棵树的右儿子,并递归这个点的右儿子,遇到大的同理,这是依据 \(BST\) 的性质做的,很难理解,再偷张图
    9agrhgfz
  • sbmqwm:如果觉得msjing讲的不清,请前往这里学一下,msjing也是学的这个,我去紫菜一下msjing
  • 一段美好的时光,请脑补
  • msjing:sbmqwm又欺负我555
  • 回归正题,这样做就可以分开树了,实现是贺的\(Super\)_\(lollipop\)的,给一下吧
点击查看代码
void Split(int now,int v,int &x,int &y)
{
	if (!now) return x=y=0,void();
	if (val[now]<=v) x=now,Split(rs[now],v,rs[now],y);
	else y=now,Split(ls[now],v,x,ls[now]);
	Pushup(now);
}
  • 里面的Pushup等会再说

\(Merge\)

  • \(Merge\)操作是把分出两棵树合并,具体如何合并呢
  • 由于我们分出的两棵树有:左子树小于右子树的性质且区间不交,故仅需搞一下我们随机出来的值就行,所以合并依据随机出来的 \(rnd\)
  • 什么是区间不交,我们看上图,左子树表示区间 \([1,5]\),右子树则是 \([6,7]\),这就是区间不交
  • 再粘一遍要不然看得费劲
    9agrhgfz
    批注 2026-07-10 105710
  • 我们看两张图上图(后图也是那篇文章的图),已经分裂好的两棵树(\(rnd\) 以下图为准),其根分别为 4 和 6,发现 \(rnd_4 < rnd_6\),所以 6 应该在 4 的下面,并向下递归其右子树到达 5,发现 \(rnd_5 > rnd_6\),所以 5 应该在 6 的下面,所以 5 成为 6 的儿子,6 成为 4 的儿子,就还原成上图了
    批注 2026-07-10 110407
  • 至此两个关键操作就搞完了
点击查看代码
int Merge(int x,int y)
{
	if (!x || !y) return x|y;
	if (rnd[x]<rnd[y])
	{
		rs[x]=Merge(rs[x],y);
		Pushup(x);
		return x;
	}
	else
	{
		ls[y]=Merge(x,ls[y]);
		Pushup(y);
		return y;
	}
}

其他操作

\(New\)

  • 新建一个节点,好搞,记得再 \(rd\) 一个权值就行
点击查看代码
int New(int v)
{
	tot++,val[tot]=v,rnd[tot]=rd(),siz[tot]=1;
	return tot;
}

\(Pushup\)

  • 上传,和线段树很像,目的是得到 \(siz\)
点击查看代码
void Pushup(int x) {siz[x]=siz[ls[x]]+siz[rs[x]]+1;}

\(Insert\)

  • 加入一个点,直接依据加的点把树裂开加然后和就行了常数起飞了
点击查看代码
void Insert(int a)
{
	int x,y;
	Split(root,a,x,y);
	root=Merge(Merge(x,New(a)),y);
}

\(Delete\)

  • 删除同理,把树分裂踢出这个点再合并就行
点击查看代码
void Delete(int v)
{
	int x,y,z;
	Split(root,v,x,z);
	Split(x,v-1,x,y);
	y=Merge(ls[y],rs[y]);
	root=Merge(Merge(x,y),z);
}

\(fid\) 全家桶

\(fid\)_\(kth\)
  • \(k\) 的排名,递归找,如果左儿子的个数比 \(k\) 小,让 \(k - siz_{ls_x} - 1\),然后递归进入右子树,否则递归进入左子树。当 \(k = siz_{ls_x}\) 的时候就找到这个值了
\(fid\)_\(siz\)
  • 找位置,暴力拆树算然后和树
\(fid\)_ \(prev\) & \(fid\)_\(nxt\)
  • 查前驱后继,暴力拆树,前驱按 \(<v\)\(\ge v\) 拆,左子树的第 \(siz_x\) 大(最大的)就是了,后继同理,按 \(\le v\)\(> v\) 拆,右子树第一小就是
点击查看代码
int fid_kth(int x,int k)
{
	while (1)
	{
		if (k<=siz[ls[x]]) x=ls[x];
		else if (k == siz[ls[x]]+1) return x;
		else k-=(siz[ls[x]]+1),x=rs[x];
	}
}
int fid_siz(int v)
{
	int x,y;
	Split(root,v-1,x,y);
	int k=siz[x]+1;
	root=Merge(x,y);
	return k;
}
int fid_prev(int v)
{
	int x,y;
	Split(root,v-1,x,y);
	int tmp=val[fid_kth(x,siz[x])];
	root=Merge(x,y);
	return tmp;
}
int fid_nxt(int v)
{
	int x,y;
	Split(root,v,x,y);
	int tmp=val[fid_kth(y,1)];
	root=Merge(x,y);
	return tmp;
}
  • \(kth\) 中这么做就是对的,我不会证,哎sbmqwm你别搞我
  • 又是一段美好的时光

总代码

点击查看代码
int n,root;
namespace fhq
{
	mt19937 rd(time(0));
	int val[maxn],siz[maxn],rnd[maxn],tot,ls[maxn],rs[maxn];
	void Pushup(int x) {siz[x]=siz[ls[x]]+siz[rs[x]]+1;}
	void Split(int now,int v,int &x,int &y)
	{
		if (!now) return x=y=0,void();
		if (val[now]<=v) x=now,Split(rs[now],v,rs[now],y);
		else y=now,Split(ls[now],v,x,ls[now]);
		Pushup(now);
	}
	int Merge(int x,int y)
	{
		if (!x || !y) return x|y;
		if (rnd[x]<rnd[y])
		{
			rs[x]=Merge(rs[x],y);
			Pushup(x);
			return x;
		}
		else
		{
			ls[y]=Merge(x,ls[y]);
			Pushup(y);
			return y;
		}
	}
	int New(int v)
	{
		tot++,val[tot]=v,rnd[tot]=rd(),siz[tot]=1;
		return tot;
	}
	void Insert(int a)
	{
		int x,y;
		Split(root,a,x,y);
		root=Merge(Merge(x,New(a)),y);
	}
	int fid_kth(int x,int k)
	{
		while (1)
		{
			if (k<=siz[ls[x]]) x=ls[x];
			else if (k == siz[ls[x]]+1) return x;
			else k-=(siz[ls[x]]+1),x=rs[x];
		}
	}
	int fid_siz(int v)
	{
		int x,y;
		Split(root,v-1,x,y);
		int k=siz[x]+1;
		root=Merge(x,y);
		return k;
	}
	int fid_prev(int v)
	{
		int x,y;
		Split(root,v-1,x,y);
		int tmp=val[fid_kth(x,siz[x])];
		root=Merge(x,y);
		return tmp;
	}
	int fid_nxt(int v)
	{
		int x,y;
		Split(root,v,x,y);
		int tmp=val[fid_kth(y,1)];
		root=Merge(x,y);
		return tmp;
	}
	void Delete(int v)
	{
		int x,y,z;
		Split(root,v,x,z);
		Split(x,v-1,x,y);
		y=Merge(ls[y],rs[y]);
		root=Merge(Merge(x,y),z);
	}
};

后记

  • msjing还有 \(Splay\) 要学,\(Splay\) 的学习笔记等学会了再写
  • 以下是捏造在说话
    • 自?己!屮~自>>>己???
posted @ 2026-07-10 11:23  msjing  阅读(23)  评论(4)    收藏  举报