平衡树 - FHQ 学习笔记
平衡树 - FHQ 学习笔记
主要参考万万没想到 的 FHQ-Treap学习笔记。
本片文章的姊妹篇:平衡树 - Splay 学习笔记。
感觉完全不会平衡树,又重新学习了一遍 FHQ,一口气把常见套路都学完了。
一、大致内容及分类
FHQ(???),全称非旋转 Treap,是一种可以用于维护按权值、排名分裂的数据结构。它相比与 Splay 虽然常数较大,但是实现起来代码难度相对容易,而且由于它非旋的特点,也可以用来实现可持久化。
既然叫做非旋 Treap,它兼有 Treap 的特点又有非旋转独特的优势。
- 从 Treap 角度看,他们同样都是依赖修正值
rnd
是随机的,用将他们按照rnd
形成一个小根堆。与 Treap 相同,它也满足笛卡尔树的性质,它的中序遍历和它的插入顺序相同,即 \(1\) 到 \(n\) 的序列。 - 从非旋角度看,FHQ 直接通过
split
和merge
操作实现添加、删除元素,不用再树上旋转了。
根据不同题目要求,将平衡树分为序列平衡树和权值平衡树。
- 序列平衡树的中序遍历为每个元素在序列中的下标,权值为每个元素具体的值,常见题型为区间翻转等。
- 权值平衡树的中序遍历是所有元素从小到大排序,即按照中序遍历提取所有元素后元素权值递增,常见操作为第 \(k\) 大等。
如果毒瘤出题人同时综合了以上两种操作,即区间翻转 \(+\) 区间第 \(k\) 大,应该怎么做呢?好吧,如果真是这样,这篇文章可能不能够帮到你,直接上一个根号做法!
二、基本操作
FHQ 的核心操作就是 split
出操作区间,操作完后 merge
回去。
下边讲解中默认的平衡树类型为权值平衡树,序列平衡树其实是将某些 \(val\) 改为了 \(siz\)。
分裂 split
无论是按照排名还是权值分裂,他们都是将原树分为左右两半,可以利用中序遍历的性质进行分裂。
具体操作时,我们新建两个临时变量 \(x,y\) 分别表示分裂出来的左边、右边的那颗平衡树。
如果我们遇到一个应该属于 \(x\) 树的节点,就将这个点以及这个点的左子树加入 \(x\) 树中,并递归分裂右子树;如果遇到属于 \(y\) 的,就将这个点与它的右子树加入 \(y\) 树中,并递归分裂左子树。
需要注意到是,如果使用序列平衡树,下面和 \(k\) 的大小判断应该变为 \(ls.siz+1\le k\)。
\(\bigstar\texttt{Attention}\):如果需要 split
一个区间,记得先 split
右端点再 split
左端点!
可以写出伪代码如下:
void split(int p,int k,int &x,int &y) // 分裂出 (-infty,k],(k,+infty)
{if(!p) { x=y=0; return; }pushdown(p);if(tree[p].val<=k) x=p,split(rs,k,rs,y);else y=p,split(ls,k,x,ls);pushup(p);
}
合并 merge
由于这是 FHQ 的 merge
,需要在合并时既保证小根堆性质又不破坏中序遍历的特点,对合并的两棵树有特殊的要求:左右区间不能够相交或者顺序颠倒!
所以我们在合并时必须按照顺序从左到右合并。
具体操作时,可以直接将 rnd
小的作为新树的根节点,如果这个根节点来自左子树就递归右子树,相反来自右子树就递归左子树(由于满足上面区间不相交也不颠倒的特点)。
写出伪代码:
int merge(int x,int y)
{if(!x || !y) return x+y;if(tree[x].rnd<tree[y].rnd){ pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x); return x; }else{ pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y); return y; }
}
新建节点 new
没什么可说的,就是给新节点附一个随机的 rnd
。
inline int New(ll Val)
{int p=++All;tree[p].rnd=rand(),tree[p].val=Val;tree[p].siz=tree[p].cnt=1;tree[p].pl=tree[p].pr=0;return p;
}
插入 insert
直接分裂出两端区间,把新建的加点放到两棵树中间在合并即可。
inline void Insert(ll Val)
{split(root,Val,x,y);root=merge(merge(x,New(Val)),y);
}
删除 delete
FHQ 可以实现删除一个数或删除这个值的所有数,唯一区别就在于分裂时的不同。
inline void Delete_one(ll Val)
{split(root,Val,x,z),split(x,Val-1,x,y);y=merge(tree[y].pl,tree[y].pr);root=merge(merge(x,y),z);
}
inline void Delete_All(ll Val)
{split(root,Val,x,z),split(x,Val-1,x,y);root=merge(x,z);
}
查询排名对应权值 Rank_to_Value
根据每颗子树的 \(siz\) 暴力跳即可。
inline int kth(int p,int Rank) // 这里返回的是找到节点的下标
{while(p){if(tree[ls].siz>=Rank) p=ls;else if(tree[ls].siz+tree[p].cnt>=Rank) return p;else Rank-=tree[ls].siz+tree[p].cnt,p=rs;}return p;
}
查询权值对应排名 Value_to_Rank
按照权值分裂出来后左区间树的 \(siz\) 就是排名。
inline int Value_to_Rank(ll Value)
{split(root,Value-1,x,y);int ret=tree[x].siz+1;root=merge(x,y);return ret;
}
查询前驱 Findpre
分裂出来后在左子树中排名最靠后的是前驱。
inline ll Findpre(ll Value)
{split(root,Value-1,x,y);ll ret=tree[kth(x,tree[x].siz)].val;root=merge(x,y);return ret;
}
查询后继 Findnex
分裂出来后在右子树中排名最靠前的是后继。
inline ll Findnex(ll Value)
{split(root,Value,x,y);ll ret=tree[kth(y,1)].val;root=merge(x,y);return ret;
}
三、可持久化平衡树
平衡树上的可持久化和线段树的可持久化其实差别不大,每次修改的时候需要建立新的节点,对于每个版本也需要保存根节点。
新建一个点的原则:如果我们把版本最新的点叫做新点,那么我们只能够在可持久化平衡树中对新点修改,不然就会出错,所以在 split
与 merge
中,我们一旦对一个值进行了修改,就需要新建一个节点。
伪代码如下:
inline void pushdown(int p)
{if(!tree[p].tag) return;if(ls) ls=clone(ls),tree[ls].tag^=1,swap(tree[ls].pl,tree[ls].pr);if(rs) rs=clone(rs),tree[rs].tag^=1,swap(tree[rs].pl,tree[rs].pr);tree[p].tag=false;
}
inline int clone(int p) { tree[++All]=tree[p]; return All; }
void split(int p,ll k,int &x,int &y)
{if(!p) { x=y=0; return; }pushdown(p);if(tree[p].val<=k) x=clone(p),split(rs,k,tree[x].pr,y),pushup(x);else y=clone(p),split(ls,k,x,tree[y].pl),pushup(y);
}
int merge(int x,int y)
{if(!x || !y) return x+y;if(tree[x].rnd<tree[y].rnd){pushdown(x),x=clone(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);return x;}else{pushdown(y),y=clone(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);return y;}
}
四、常见优化技巧
垃圾回收
在每次新建节点的时候先从垃圾桶中捡,如果垃圾捡光了再新开节点。
笛卡尔树方式建树
由于我们的 FHQ 是一种笛卡尔树,所以如果给定了一堆点,完全可以直接用笛卡尔树的方式 \(\mathcal{O(n)}\) 建树。
inline int build()
{int tp=0,p=0,Last;for(int i=1;i<=n;i++){p=New(v[i]),Last=0;while(tp && tree[sta[tp]].rnd>tree[p].rnd) pushup(Last=sta[tp--]);if(tp) tree[sta[tp]].pr=p;tree[p].pl=Last,sta[++tp]=p;}while(tp) pushup(sta[tp--]);return sta[1];
}
定期重构
如果题目中对空间有限制而且不要求查询历史版本,可以定期重构。当使用的空间超过一定值的时候,我们中序遍历整棵树并放入数组中,在线性建树。
启发式合并
如果需要合并两个有交集的 Treap 时该怎么做?我们可以每次将较小的数合并到较大的树中去,这样每个点最多只会合并 \(\log n\) 次,每次合并复杂度 \(n\log n\),总时间复杂度 \(\mathcal{O(n\log ^2 n)}\)。
区间缩点
详见万万没想到的博客,咕咕咕。
五、模板
struct FHQ_number
{#define Maxn 点数#define ls tree[p].pl#define rs tree[p].prprivate:int All=0,root=0;struct NODE { int pl,pr,siz,cnt,rnd; ll val; };NODE tree[Maxn];inline int Dot() { return ++All; }inline int New(ll Val){int p=Dot();tree[p].rnd=rand(),tree[p].val=Val;tree[p].siz=tree[p].cnt=1;tree[p].pl=tree[p].pr=0;return p;}inline void pushdown(int p) { p--; }inline void pushup(int p){ tree[p].siz=tree[ls].siz+tree[rs].siz+tree[p].cnt; }void split(int p,int k,int &x,int &y){if(!p) { x=y=0; return; }pushdown(p);if(tree[p].val<=k) x=p,split(rs,k,rs,y);else y=p,split(ls,k,x,ls);pushup(p);}int merge(int x,int y){if(!x || !y) return x+y;if(tree[x].rnd<tree[y].rnd){pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);return x;}else{pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);return y;}}inline int kth(int p,int Rank){while(p){if(tree[ls].siz>=Rank) p=ls;else if(tree[ls].siz+tree[p].cnt>=Rank) return p;else Rank-=tree[ls].siz+tree[p].cnt,p=rs;}return p;}int x,y,z;public:inline void Insert(ll Val){ split(root,Val,x,y),root=merge(merge(x,New(Val)),y); }inline void Delete_one(int Val){split(root,Val,x,z),split(x,Val-1,x,y);y=merge(tree[y].pl,tree[y].pr);root=merge(merge(x,y),z);}inline ll Rank_to_Value(int Rank){ return tree[kth(root,Rank)].val; }inline int Value_to_Rank(ll Value){split(root,Value-1,x,y);int ret=tree[x].siz+1;root=merge(x,y);return ret;}inline ll Findpre(ll Value){split(root,Value-1,x,y);ll ret=tree[kth(x,tree[x].siz)].val;root=merge(x,y);return ret;}inline ll Findnex(ll Value){split(root,Value,x,y);ll ret=tree[kth(y,1)].val;root=merge(x,y);return ret;}
}T;
struct FHQ_sequence
{#define Maxn 点数#define ls tree[p].pl#define rs tree[p].print All=0,root=0;struct NODE { int pl,pr,siz,cnt,rnd,val; bool tag; };NODE tree[Maxn];inline int Dot() { return ++All; }inline int New(int Val){int p=Dot();tree[p].rnd=rand(),tree[p].val=Val;tree[p].cnt=tree[p].siz=1;tree[p].pl=tree[p].pr=0;return p;}inline void pushdown(int p){if(!tree[p].tag) return;swap(tree[ls].pl,tree[ls].pr);swap(tree[rs].pl,tree[rs].pr);tree[ls].tag^=1,tree[rs].tag^=1;tree[p].tag=false;}inline void pushup(int p){ tree[p].siz=tree[ls].siz+tree[p].cnt+tree[rs].siz; }void split(int p,int k,int &x,int &y){if(!p) { x=y=0; return; }pushdown(p);if(tree[ls].siz<k) x=p,split(rs,k-tree[ls].siz-1,rs,y);else y=p,split(ls,k,x,ls);pushup(p);}int merge(int x,int y){if(!x || !y) return x+y;if(tree[x].rnd<tree[y].rnd){pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);return x;}else{pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);return y;}}int kth(int p,int Rank){while(p){if(tree[ls].siz>=Rank) p=ls;else if(tree[ls].siz+tree[p].cnt>=Rank) return p;else Rank-=tree[ls].siz+tree[p].cnt,p=ls;}return p;}void Insert(int Val) // 插到末尾 { root=merge(root,New(Val)); }int x,y,z;inline void Reverse(int l,int r){split(root,r,x,z),split(x,l-1,x,y);swap(tree[y].pl,tree[y].pr),tree[y].tag^=1;root=merge(merge(x,y),z);}void print(int p){pushdown(p);if(ls) print(ls);printf("%d ",tree[p].val);if(rs) print(rs);}
}T;
六、例题
【模板】普通平衡树
【模板】普通平衡树(数据加强版)
【模板】文艺平衡树
【模板】可持久化平衡树
【模板】可持久化文艺平衡树
平衡树题单
平衡树 - FHQ 学习笔记相关推荐
- 文艺平衡树 Splay 学习笔记(1)
(这里是Splay基础操作,reserve什么的会在下一篇里面讲) 好久之前就说要学Splay了,结果苟到现在才学习. 可能是最近良心发现自己实在太弱了,听数学又听不懂只好多学点不要脑子的数据结构. ...
- 平衡树学习笔记之 fhq Treap
平衡树学习笔记 1:fhq Treap(非旋 Treap) 正文开始前首先 %%% fhq 大佬. 众所周知,平衡树是一种 非常猥琐 码量堪忧的数据结构. 他的祖先是一种叫做二叉搜索树 ( B S T ...
- Treap平衡树学习笔记
Treap平衡树学习笔记 放在前面的话 与信息学竞赛告别9个月后,中考终于结束,我终于复出了... 结果一回来就学数据结构,学得我有点懵... 本文写了两个多星期... 尽管学习Treap感到万分痛苦 ...
- 平衡树(splay)学习笔记(详细,从入门到精(bao)通(ling))(持续更新)
前言 在前几天军训站军姿的时候胡思乱想,突然明白了splay的本质 KMP学习笔记后又一篇字数上万的题解- 前置技能--二叉搜索树 首先来看一个最简单的问题: 你需要维护一个数据结构,资磁这些操作: ...
- pbds库学习笔记(优先队列、平衡树、哈希表)
目录 pbds库学习笔记(优先队列.平衡树.哈希表) 前言 概述 priority_queue优先队列 概述 参数 堆的基本操作的函数 对比STL新增函数 modify修改 Dijkstra最短路径演 ...
- [学习笔记]可持久化数据结构——数组、并查集、平衡树、Trie树
可持久化:支持查询历史版本和在历史版本上修改 可持久化数组 主席树做即可. [模板]可持久化数组(可持久化线段树/平衡树) 可持久化并查集 可持久化并查集 主席树做即可. 要按秩合并.(路径压缩每次建 ...
- MySQL服务器学习笔记!(二) ——数据库各项操作
原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://foreveryan.blog.51cto.com/3508502/657640 ...
- l2-004 这是二叉搜索树吗?_算法学习笔记(45): 二叉搜索树
二叉搜索树(Binary Search Tree, BST)是一种常用的数据结构,在理想情况下,它可以以 的复杂度完成一系列修改和查询,包括: 插入一个数 删除一个数 查询某数的排名(排名定义为比该数 ...
- 【学习笔记】线段树详解(全)
[学习笔记]线段树详解(全) 和三个同学一起搞了接近两个月的线段树,头都要炸了T_T,趁心态尚未凉之前赶快把东西记下来... [目录] [基础]作者:\((Silent\)_\(EAG)\) [懒标记 ...
最新文章
- Python判断字符串是否为字母或者数字
- 通过单步调试理解Angular里routerLink指令实际url的生成逻辑
- 如何对 Jenkins 共享库进行单元测试
- 如何提升应用程序启动权限
- Leetcode 214.最短回文串
- VMware Converter Standalone结合TrueImage 迁移HyperV虚机
- springboot源码 红色J_通达信精准指标,精确箱体——(主图 源码)介绍
- Selenium +Python项目实践(注册流程)
- matlab三轴定位程序,三边测量定位MATLAB源码
- 【Java】 牛客网华为机试108题汇总
- 50多首经典的广播电台背景音乐推荐下载
- 特征函数(characteristic function)
- 【愚公系列】2023年06月 网络安全(交通银行杯)-疑惑的汉字
- Qt Creator 运行LVGL模拟器
- java中library找不到了,java web 找不到java.library.path途径
- bootstrap 图标系列
- 利用ViewPager和WheelView实现横向纵向轮番滚动
- 史上最长最全!围绕故障管理谈SRE体系建设
- 不懂技术,自己如何做网站?
- Java分布式中文分词组件 - word分词(转自:https://github.com/ysc/word)
热门文章
- spss22.0统计分析从入门到精通_数据分析最全资料:SPSS/MATLAB/SQL/SAS/EXCEL经典教材+视频教程,快速入门!...
- linux应用与管理,Linux操作系统应用与管理
- vue 插入word模板 项目_10 分钟为你的 vue 项目编写代码文档
- java实用教程——组件及事件处理——处理事件
- Leetcode周赛复盘——第 278 场力扣周赛
- Java Number Math 类方法
- [PAT乙级]1018 锤子剪刀布
- [蓝桥杯2015决赛]机器人数目-枚举
- Tree Recovery(二叉树递归遍历+求后序遍历模板)
- Cheapest Palindrome POJ - 3280(动态规划*)