【Study】FHQ-Treap
其实是复习笔记(
简介
是平衡树的一种,主要沿袭了 的思路,与其他平衡树不同的是它并非进行旋转操作来维护树的平衡,而是进行分裂和合并的操作,因此又称非旋 。
优点:码量少,易理解,灵活
缺点:常数略大
实现
先说明一点东西:
int rt;//根节点
int L,R;//后面会提到
int cnt;//节点编号
struct node{
int l,r;//左右儿子
int val;//当前节点的权值
int key;//随机生成的附属值
int siz;//子树(包括自己)的节点数量
}t[MAXN];
1、新建节点,维护节点数
无需多言。
int Addn(int a)
{
t[++cnt].siz=1;
t[cnt].val=a;
t[cnt].key=rand();
return cnt;
}
#define ls t[u].l
#define rs t[u].r
void Pushup(int u)
{
t[u].siz=t[ls].siz+t[rs].siz+1;
}
2、分裂()
主要思想就是将一棵树分为两颗
有两种类型,分为 按 分裂 和 按 分裂 两种。
(对于上面的模板题需要按 分裂)
这里仔细将按 分裂
首先我们需要定义 , 分别表示左右子树
规定一个关键值 ,将 小于等于 的节点放到左子树,将 大于 的放到右子树
代码如下:
#define ls t[u].l
#define rs t[u].r
void Split(int u,int k,int &L,int &R)
//u表示当前节点,L和R分别表示左右子树所新增的节点,但注意,此处的L、R仍不存在,为虚拟节点
//注意此处需要引用 & ,才能将虚拟节点修改为真正的节点
{
if(!u){L=R=0;return;}
//若当前节点不存在,便无需加上新的节点
if(t[u].val<=k) L=u,split(rs,k,rs,R);
//如果当前节点的val小于等于k,则将u及u的左子树放入L,继续遍历u的右子树
//但是右子树可能会有节点的val小于等于k且大于t[u].val,所以u的右儿子可能需要修改,于是有split(_,_,rs,_)
//假如t[rs].val仍然小于等于k,则L=u,发现实际对于上一层遍历就是将t[u].rs修改为t[u].rs
else R=u,split(ls,k,L,ls);
//如果当前节点的val大于k,则将u及u的右子树放入R,继续遍历u的左子树
//剩下的其实同理
pushup(u);
}
对于 int &L,int &R 的理解是个难点也是关键点,其实配合图片进行理解会较容易,比如这篇
洛谷日报
对分裂和合并的操作有详细的图解。
对于按 分裂也是类似的,这里就不细讲了qwq
#define ls t[u].l
#define rs t[u].r
void Split(int u,int k,int &L,int &R)
{
if(!u){L=R=0;return;}
if(t[ls].siz+1<=k) L=u,split(rs,k-t[ls].siz-1,rs,R);
else R=u,split(ls,k,L,ls);
pushup(u);
}
2、合并()
理解了 操作后对于 操作也就不难理解了
此时就是需要将分裂出来的 进行合并,因为两颗子树都满足 的性质,我们就仅需对比此时两颗需合并子树的 值,进行合并,来维护合并后的新树的平衡
可以结合代码进行理解:
int Merge(int L,int R)
{
if(!L||!R) return L|R;
//如果其中一颗子树为空,则直接合并
if(t[L].key<=t[R].key)
//如果左子树的根的key小于等于右子树的根的key
{
t[L].r=merge(t[L].r,R);
//则将左子树的右子树与右子树进行合并,以保证小根堆的性质
//其实左子树的左子树一定小于右子树,并且左子树的左子树已经满足小根堆的性质,就无需进行修改
//这里有点绕qwq
pushup(L);
//因为处理的是左子树,进行更新
return L;
//返回根节点
}
else
{
//接下来均同理
t[R].l=merge(L,t[R].l);
pushup(R);
return R;
}
}
理解掌握了 和 , 接下来的其实就很容易了
3、插入
void Insert(int a)
{
Split(rt,a,L,R);
rt=merge(merge(L,addn(a)),R);
}
挺好理解,将整棵树拆为 节点小于等于插入值的子树 和 节点大于插入值的子树 两部分,再依次将子树和节点、子树和子树进行合并。
4、删除
int x;
void Delete(int a)
{
split(rt,a,L,R);
split(L,a-1,L,x);
rt=merge(L,R);
}
这里的 和 , 同种作用,在这里辅助处理
这个看代码就能理解的吧qwq
但是在上面的模板题,里面的删除操作要求删除一个,而我们这里对所有权值为 的节点都进行删除了,所以需要小修改一下
int x;
void Delete(int a)
{
split(rt,a,L,R);
split(L,a-1,L,x);
x=merge(t[x].l,t[x].r);
//抛弃这个节点,将这个节点的左右子树进行合并
rt=merge(merge(L,x),R);
}
5、查询指定数的排名
int Findrank(int a)
{
split(rt,a-1,l,r);
int ans=t[l].siz+1;
rt=merge(l,r);
//注意查询完要合并qwq
return ans;
}
排名定义为比当前数小的数的个数 +1
根据题目定义直接找
6、查询指定排名的数
#define ls t[u].l
#define rs t[u].r
int Findkth(int u,int k)
{
while(1)
{
if(k<=t[ls].siz) u=ls;
else if(k==t[ls].siz+1) return t[u].val;
else k=k-t[ls].siz-1,u=rs;
//注意这里k要减去左边的节点数
}
}
qwq看代码吧
7、查询前驱/后继
int Findpre(int a)
{
split(rt,a-1,l,r);
int ans=findkth(l,t[l].siz);
rt=merge(l,r);
return ans;
}
int Findsuf(int a)
{
split(rt,a,l,r);
int ans=findkth(r,1);
rt=merge(l,r);
return ans;
}
对于前驱,分裂出值小于 的子树,在查询这个子树的最大值。
因为这颗子树满足Treap性质所以只要找这个子树对最后一名就行。
对于后驱,同理qwq
总结
到这里我们就真正处理完一颗 的基本操作。
可以发现 是可以对区间进行处理的,假如我们要处理区间 ,只需把整棵树分裂为 和 ,再将 分裂为 和 ,然后对分裂出来的区间进行乱搞就行了qwq
所以 的灵活之处就在此,你可以随心所欲地对某些东西进行十分轻松的维护处理,这使 在很多题目上都能大显神通 qwq
(但是在开心地刷题之余记得对常数进行优化qwq
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通