算法随笔——平衡树 FHQ-Treap(附笛卡尔树)
参考博客:https://www.cnblogs.com/TimelessWelkinBlog/p/17610065.html
https://blog.csdn.net/luhaoren2009/article/details/131880277
https://henryhuang.blog.luogu.org/solution-p4402#
https://blog.csdn.net/yyh_getAC/article/details/125710091
感谢大佬的博客。
FHQ-Treap 介绍
FHQ-Treap 可以说是最强大的平衡树之一,具有马亮小、拓展性强等特点,其简洁的写法让人们浴霸不能。
FHQ-Treap 本质上还是一颗 Treap 树,但没有 Treap 中令人头晕的左转右转,取而代之的只有两个简洁的函数
分裂
分裂,又称
可分为按权值分裂和按大小分裂两种操作。
按权值分裂
将原树分裂为一棵权值均小于等于
可以把这里的
// u是递归的当前节点,val是值.
// l是分裂的左树(<=val)当前的节点,r是右树(>val)当前的节点。
void split(int u,int val,int &l,int &r) //按照权值分裂
{
if (u == 0) {l = r = 0; return;}
//比val说明左子树都小,全部分裂到左边
//这里递归的l = t[u].r 是因为已经接上了u,继续从其左子树递归。
if (t[u].val <= val) l = u,split(t[u].r,val,t[u].r,r);
else r = u,split(t[u].l,val,l,t[u].l); //同理
pushup(u);
}
见图:
顺便看一下 pushup 操作:
il void pushup(int x)
{
t[x].size = t[t[x].l].size + t[t[x].r].size + 1;
}
按大小分裂
其实也是一样的,就是分裂成一棵大小为
void split(int u,int siz,int &l,int &r)
{
if (u == 0){l = r = 0;return;}
pushdown(u);
//size要减去左子树加自己
if (t[t[u].l].size < siz)l = u,split(t[u].r,siz-t[t[u].l].size-1,t[u].r,r);
else r = u,split(t[u].l,siz,l,t[u].l);
pushup(u);
}
合并
是将两个树合并在一起的操作。
这里需要用到 Treap 的经典思想:随机,通过随机权值使得整棵树更加平衡。
在这里要保证l的所有节点均小于等于r的任何节点
int merge(int l,int r) //l,r分别指左右子树
{
if (!l || !r) return l + r;
if (t[l].dat < t[r].dat) // dat 随机值
{
t[r].l = merge(l,t[r].l); //将r的左子树和l相合并
pushup(r); //同Treap的pushup
return r;
}
else
{
t[l].r = merge(t[l].r,r); //同上
pushup(l);
return l;
}
}
拓展操作
自此,FHQ-Treap 的所有主要函数就没了,一共就两个。
但根据这两个函数的组合,可以拓展出很多操作。
插入
思路:要插入
void insert(int x)
{
int l,r;
split(root,x,l,r);
New(x);
root = merge(merge(l,idx),r); //记得更新根节点
}
注意 Treap 树中是没有
删除
思路:分裂出
int delnum(int x)
{
int L,M,R;
split(root,x,L,R);
split(L,x-1,L,M); // x-1 < M <= x 即 M树均为x
M = merge(t[M].l,t[M].r);//合并左右子树即删掉根节点
root = merge(merge(L,M),R);//合并回去
/*
一次删除所有x:
root = merge(L,R);
*/
return root;
}
通过权值查询排名
思路:直接分裂出一棵
int getrank(int val)
{
int L,R,res;
split(root,val-1,L,R);
res = t[L].size + 1;
root = merge(L,R);
return res;
}
根据排名查询权值
思路:与 Treap 中的一样,递归查询,若左边超过了该排名,则递归左边。否则递归右边。
int getval(int u,int rank)
{
if (rank == t[t[u].l].size + 1) return u;
if (rank <= t[t[u].l].size) return getval(t[u].l,rank);
return getval(t[u].r,rank-t[t[u].l].size-1);
}
求前驱
思路:分裂出一棵
int pre(int x)
{
int L,R,res;
split(root,x-1,L,R);
res = t[getval(L,t[L].size)].val;
root = merge(L,R);
return res;
}
求后继
思路:分裂出一棵
int suc(int x)
{
int L,R,res;
split(root,x,L,R);
res = t[getval(R,1)].val;
root = merge(L,R);
return res;
}
FHQ-Treap 例题
【模板】普通平衡树
题目描述
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入一个数
。 - 删除一个数
(若有多个相同的数,应只删除一个)。 - 定义排名为比当前数小的数的个数
。查询 的排名。 - 查询数据结构中排名为
的数。 - 求
的前驱(前驱定义为小于 ,且最大的数)。 - 求
的后继(后继定义为大于 ,且最小的数)。
对于操作 3,5,6,不保证当前数据结构中存在数
平衡树模板,直接见代码。
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
struct node
{
int l,r,val,dat,size;
}t[N];
int n,idx,root;
int New(int x) //新建节点并初始化
{
idx++;
t[idx].val = x;
t[idx].dat = rand();
t[idx].size = 1;
t[idx].l = t[idx].r = 0;
return idx;
}
void pushup(int x) // 更新size
{
t[x].size = t[t[x].l].size + t[t[x].r].size + 1;
}
// u是递归的当前节点,val是值.
// x是分裂的左树(<=val),y是右树(>val)。
void split(int u,int val,int &l,int &r) //按照权值分裂
{
if (u == 0) {l = r = 0; return;}
//比val说明左子树都小,全部分裂到左边
if (t[u].val <= val) l = u,split(t[u].r,val,t[u].r,r);
else r = u,split(t[u].l,val,l,t[u].l);
pushup(u);
}
int merge(int l,int r)
{
if (!l || !r) return l + r;
if (t[l].dat < t[r].dat) //按照随机权值合并
{
t[r].l = merge(l,t[r].l);
pushup(r);
return r;
}
else
{
t[l].r = merge(t[l].r,r);
pushup(l);
return l;
}
}
void insert(int x)
{
int l,r;
split(root,x,l,r);
New(x);
root = merge(merge(l,idx),r);
}
int delnum(int x)
{
int L,M,R;
split(root,x,L,R);
split(L,x-1,L,M); // x-1 < M <= x 即 M树均为x
M = merge(t[M].l,t[M].r);//合并左右子树即删掉根节点
root = merge(merge(L,M),R);//合并回去
/*
一次删除所有x:
root = merge(L,R);
*/
return root;
}
int getrank(int val)
{
int L,R,res;
split(root,val-1,L,R);
res = t[L].size + 1;
root = merge(L,R);
return res;
}
int getval(int u,int rank)
{
if (rank == t[t[u].l].size + 1) return u;
if (rank <= t[t[u].l].size) return getval(t[u].l,rank);
return getval(t[u].r,rank-t[t[u].l].size-1);
}
int pre(int x)
{
int L,R,res;
split(root,x-1,L,R);
res = t[getval(L,t[L].size)].val;
root = merge(L,R);
return res;
}
int suc(int x)
{
int L,R,res;
split(root,x,L,R);
res = t[getval(R,1)].val;
root = merge(L,R);
return res;
}
int main()
{
scanf("%d",&n);
while (n--)
{
int opt,x;
scanf("%d%d",&opt,&x);
if (opt == 1) insert(x);
else if (opt == 2) delnum(x);
else if (opt == 3) printf("%d\n",getrank(x));
else if (opt == 4) printf("%d\n",t[getval(root,x)].val);
else if (opt == 5) printf("%d\n",pre(x));
else printf("%d\n",suc(x));
}
return 0;
}
P3391 【模板】文艺平衡树
链接
一句话题意:维护一个序列,支持区间翻转操作。
这里需要用到 split 操作中的按照大小分裂。
设翻转区间为
将原树分裂出
再思考区间翻转,因为 BST 中序遍历后得到原序列,所以翻转其本质上就是每个节点从上到下交换左右子树,相当于中序遍历从左中右的顺序变成右中左,即区间翻转。
同时,我们肯定不能用暴力一个一个节点去交换,可以借用线段树中 lazytag 的思想,这样这题就搞定了。
// Problem: P3391 【模板】文艺平衡树
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3391
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Eason
// Date:2024-01-20 08:11:57
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
int n,m,a[N];
struct node
{
int val,dat,size,l,r,tag;
}t[N];
int idx = 0,root = 0;
int New(int x)
{
++idx;
t[idx].val = x;
t[idx].dat = rand();
t[idx].l = t[idx].r = 0;
t[idx].size = 1;
return idx;
}
void pushup(int x)
{
t[x].size = t[t[x].l].size + t[t[x].r].size + 1;
}
void pushdown(int x)
{
if (t[x].tag)
{
swap(t[x].l,t[x].r);
t[x].tag = 0;
t[t[x].l].tag ^= 1;
t[t[x].r].tag ^= 1;
}
}
void split(int u,int siz,int &l,int &r)
{
if (u == 0){l = r = 0;return;}
pushdown(u);
if (t[t[u].l].size < siz) l = u,split(t[u].r,siz-t[t[u].l].size-1,t[u].r,r);
else r = u,split(t[u].l,siz,l,t[u].l);
pushup(u);
}
int merge(int l,int r) //l,r分别指左右子树
{
if (!l || !r) return l + r;
if (t[l].dat < t[r].dat) // dat 随机值
{
pushdown(r); //先下传标记
t[r].l = merge(l,t[r].l); //将r的左子树和l相合并
pushup(r); //同Treap的pushup
return r;
}
else
{
pushdown(l);
t[l].r = merge(t[l].r,r); //同上
pushup(l);
return l;
}
}
void output(int x)
{
if (!x) return;
pushdown(x);
output(t[x].l); //中序遍历输出
printf("%d ",t[x].val);
output(t[x].r);
}
int main()
{
cin >> n >> m;
for (int i = 1;i <= n;i++)
root = merge(root,New(i)); //按照顺序插入平衡树
for (int i = 1;i <= m;i++)
{
int l,r ;
cin >> l >> r;
int L,M,R;
split(root,r,L,R);
split(L,l-1,L,M);
t[M].tag ^= 1;
root = merge(merge(L,M),R);
}
output(root);
return 0;
}
[Cerc2007] robotic sort 机械排序
题目描述
SORT公司是一个专门为人们提供排序服务的公司,该公司的宗旨是:“顺序是最美丽的”。他们的工作是通过一系列移动,将某些物品按顺序摆好。他们的工作规定只能使用如下方法排序:
先找到编号最小的物品的位置P1,将区间[1,P1]反转,再找到编号第二小的物品的位置P2,将区间[2,P2]反转.........
上图是有6个物品的例子,编号最小的一个是在第4个位置。因此,最开始把前面4个物品反转,第二小的物品在最后一个位置,所以下一个操作是把2-6的物品反转,第三步操作是把3-4的物品进行反转……
在数据中可能存在有相同的编号,如果有多个相同的编号,则按输入的原始次序操作。
暂略。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探