鲜花

一些鲜花放在前面,平衡树学了很久,但是每学一遍都忘,原因就在于我只能 70% 理解 + 30% 背板子,所以每次都忘。这次我采取了截然不同的策略,自己按照自己的理解打一遍,大获成功(?),大概打 20 min,调 10 min 结束,然后写下了这篇文章。

虽然但是,感觉 Treap 还是很强的,代码好写好调,而且可以解决很多问题(下面将会提到),好像说是常数大一点?无伤大雅吧……

Treap

首先,从 Treap 的定义开始。Treap 实际上是一种笛卡尔树(笛卡尔树可以看 这篇文章,每个点有两个信息 \((v_u,p_u)\),分别表示点 \(u\) 的权值与优先度。\(v_u\) 形成一个二叉搜索树(左儿子的值 \(\leq\) 自己的值 \(\leq\) 右儿子的值),\(p_u\) 形成一个大根堆(自己的值 \(\geq\) 左右儿子的值)。

你可以利用 Treap 做很多有用的操作,但是 Treap 有一个缺点,就是如果被卡成链怎么办?

FHQ Treap

这时候就需要用到 FHQ Treap,它基于 Treap 的基础上对每个点的 \(p\) 值都进行了随机化操作,这样就可以达到平衡的目的了。为什么?因为随机出来不平衡的概率很小,大多随机数都是无规律的,这样通过大根堆的性质就可以使它达到平衡,这样树高期望就是 \(O(\log n)\) 层的了,并且无法卡掉,因为随机数是程序生成的,并非数据所决定。

系统随机函数若以 srand(time(0)) 为随机种子,效果不佳,推荐使用 mt19937 rnd(233),生成随机数更加均匀。

FHQ Treap 又叫无旋 Treap,它只需要两个核心操作 merge()split() 即可完成所有的复杂操作,就像玩拼图一样把一棵树拆开再拼起来一样。下面就来具体介绍一下这两个函数到底是如何实现的。

关于 upd 函数的说明

(这个可以学完下面的再看)upd 函数,即刷新一个节点大小的函数。在 split() 中,在分裂完子树后最后应该刷新,不然可能大小信息已经被更改而不知道。同理,在 merge() 中,合并子树后也应当刷新,调不出来一定要检查一下是否因忘记刷新而错误。

split 函数

split 函数实现的功能是把一棵树按照权值大小拆分成两棵树,具体来说,split(int cur,int k,int &x,int &y) 表示将以 cur 为根的子树拆分成两棵树 \(x\) 和 \(y\),其中 \(x\) 里的权值都 \(\leq k\),\(y\) 里的权值都 \(>k\)。

怎么写呢?首先,为了方便返回,直接将要拆分成的两棵子树引用定义在函数参数中。先特判一下,如果要拆分的树为空,则拆分出来的两棵树也为空。不然就判断树根是否 \(\leq k\),如果是,则树左子树都 \(\leq k\),即属于 \(x\),那么只要分裂右子树即可,反之亦然。

代码
  1. void split(int cur,int k,int &x,int &y)
  2. {
  3. if(!cur)
  4. {
  5. x=y=0;
  6. return;
  7. }
  8. if(tree[cur].val<=k)
  9. {
  10. x=cur;
  11. split(tree[x].rs,k,tree[x].rs,y);
  12. }
  13. else
  14. {
  15. y=cur;
  16. split(tree[y].ls,k,x,tree[y].ls);
  17. }
  18. upd(cur);
  19. }

merge 函数

merge 函数就是把两个树 \(x,y\) 合并,保证所有 \(x\) 中的 \(v\) 都 \(\leq\) 所有 \(y\) 中的 \(v\),具体来说,就是 int merge(int x,int y)

写法就是判断 \(x,y\) 中是否有空树,有的话直接返回另一个即可。不然比较两者根的有先值,如果第一个大,根据大根堆性质,显然应该合并 \(x\) 的右子树和 \(y\),反之亦然。

代码
  1. int merge(int x,int y)
  2. {
  3. if(!x||!y)
  4. return x+y;
  5. if(tree[x].key>=tree[y].key)
  6. {
  7. tree[x].rs=merge(tree[x].rs,y);
  8. upd(x);
  9. return x;
  10. }
  11. else
  12. {
  13. tree[y].ls=merge(x,tree[y].ls);
  14. upd(y);
  15. return y;
  16. }
  17. }

其他操作的实现

下面来看看 FHQ Treap 能实现哪些操作吧,请看模板题 普通平衡树。(下面全部内容为笔者自己发挥,若有更好做法请在评论区留言或私信笔者)

插入 \(x\):将根以 \(x\) 值分裂,在中间建一个新的点合并回去即可,比较容易。

删除 \(x\):将根分别以 \(x-1,x\) 分裂,中间一个树就是权值为 \(x\) 的树,把这个树替换为它左右子树合并的结果即可(因为只要删一个,相当于把根节点给删了。

查询 \(x\) 的排名:这个非常容易,直接按 \(x-1\) 分裂并输出第一个子树大小 +1 即可。

查询排名为 \(x\) 的数:这个可以从根节点开始,不停地判断应该往左子树走还是右子树走,判断方式就是看当前点地左子树大小 +1 和 \(x\) 的大小关系,若相等则直接退出。

**查询 \(x\) 的前驱:$$笔者做法比较暴力,直接将数按 \(x-1\) 分裂,第一棵树的最后一个就是,最后一个求可以查询排名为数大小的数,用之前的函数可以求出。

**查询 \(x\) 的后继:$$同前驱做法,按 \(x\) 分裂,第二棵树的第一个就是,同样查询排名为 1 的树即可,使用之前的函数。

具体的还是看代码吧。

代码
  1. #include <bits/stdc++.h>
  2. #define TIME 1e3*clock()/CLOCKS_PER_SEC
  3. using namespace std;
  4. // stay organized
  5. mt19937 rnd(233);
  6. const int maxn=1e5+10;
  7. struct Node
  8. {
  9. int ls,rs;
  10. int siz;
  11. int val,key;
  12. }tree[maxn];
  13. int rt=0,tot=0;
  14. int newnode(int val)
  15. {
  16. tot++;
  17. tree[tot].ls=tree[tot].rs=0;
  18. tree[tot].siz=1;
  19. tree[tot].val=val;
  20. tree[tot].key=rnd();
  21. return tot;
  22. }
  23. void upd(int x)
  24. {
  25. tree[x].siz=tree[tree[x].ls].siz+1+tree[tree[x].rs].siz;
  26. }
  27. void split(int cur,int k,int &x,int &y)
  28. {
  29. if(!cur)
  30. {
  31. x=y=0;
  32. return;
  33. }
  34. if(tree[cur].val<=k)
  35. {
  36. x=cur;
  37. split(tree[x].rs,k,tree[x].rs,y);
  38. }
  39. else
  40. {
  41. y=cur;
  42. split(tree[y].ls,k,x,tree[y].ls);
  43. }
  44. upd(cur);
  45. }
  46. int merge(int x,int y)
  47. {
  48. if(!x||!y)
  49. return x+y;
  50. if(tree[x].key>=tree[y].key)
  51. {
  52. tree[x].rs=merge(tree[x].rs,y);
  53. upd(x);
  54. return x;
  55. }
  56. else
  57. {
  58. tree[y].ls=merge(x,tree[y].ls);
  59. upd(y);
  60. return y;
  61. }
  62. }
  63. int x,y,z;
  64. void ins(int val)
  65. {
  66. split(rt,val,x,y);
  67. rt=merge(merge(x,newnode(val)),y);
  68. }
  69. void del(int val)
  70. {
  71. split(rt,val-1,x,y);
  72. split(y,val,y,z);
  73. y=merge(tree[y].ls,tree[y].rs);
  74. rt=merge(merge(x,y),z);
  75. }
  76. int rnk(int val)
  77. {
  78. split(rt,val-1,x,y);
  79. int ans=tree[x].siz+1;
  80. rt=merge(x,y);
  81. return ans;
  82. }
  83. int kth(int now,int k)
  84. {
  85. while(tree[tree[now].ls].siz+1!=k)
  86. {
  87. if(tree[tree[now].ls].siz+1>k)
  88. now=tree[now].ls;
  89. else
  90. {
  91. k-=tree[tree[now].ls].siz+1;
  92. now=tree[now].rs;
  93. }
  94. }
  95. return tree[now].val;
  96. }
  97. int pre(int val)
  98. {
  99. split(rt,val-1,x,y);
  100. int ans=kth(x,tree[x].siz);
  101. rt=merge(x,y);
  102. return ans;
  103. }
  104. int suf(int val)
  105. {
  106. split(rt,val,x,y);
  107. int ans=kth(y,1);
  108. rt=merge(x,y);
  109. return ans;
  110. }
  111. int main()
  112. {
  113. ios::sync_with_stdio(false);
  114. cin.tie(0);
  115. cout.tie(0);
  116. int t;
  117. cin>>t;
  118. int opt,x;
  119. while(t--)
  120. {
  121. cin>>opt>>x;
  122. if(opt==1)
  123. ins(x);
  124. else if(opt==2)
  125. del(x);
  126. else if(opt==3)
  127. cout<<rnk(x)<<'\n';
  128. else if(opt==4)
  129. cout<<kth(rt,x)<<'\n';
  130. else if(opt==5)
  131. cout<<pre(x)<<'\n';
  132. else cout<<suf(x)<<'\n';
  133. }
  134. return 0;
  135. // you should actually read the stuff at the bottom
  136. }
  137. /* stuff you should look for
  138. * int overflow, array bounds
  139. * clear the arrays?
  140. * special cases (n=1?),
  141. * WRITE STUFF DOWN,
  142. * DON'T GET STUCK ON ONE APPROACH
  143. */

完结撒花 owo~

FHQ Treap 详解的更多相关文章

  1. 【数据结构】FHQ Treap详解

    FHQ Treap是什么? FHQ Treap,又名无旋Treap,是一种不需要旋转的平衡树,是范浩强基于Treap发明的.FHQ Treap具有代码短,易理解,速度快的优点.(当然跟红黑树比一下就是 ...

  2. Treap详解

    今天一天怼了平衡树.深深地被她的魅力折服了.我算是领略到了高级数据结构的美妙.oi太神奇了. 今天初识平衡树,选择了Treap. Treap又叫树堆,是一个二叉搜索树.我们知道,它的节点插入是随机的, ...

  3. 非旋 treap 结构体数组版(无指针)详解,有图有真相

    非旋  $treap$ (FHQ treap)的简单入门 前置技能 建议在掌握普通 treap 以及 左偏堆(也就是可并堆)食用本blog 原理 以随机数维护平衡,使树高期望为logn级别, FHQ  ...

  4. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  5. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  6. EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解

    前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...

  7. Java 字符串格式化详解

    Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...

  8. Android Notification 详解(一)——基本操作

    Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...

  9. Android Notification 详解——基本操作

    Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...

随机推荐

  1. [CSP-S 2019 day2 T1] Emiya家今天的饭

    题面 题解 不考虑每种食材不超过一半的限制,答案是 减去 1 是去掉一道菜都不做的方案. 显然只可能有一种菜超过一半,于是枚举这种菜,对每个方式做背包即可(记一维状态表示这种菜比别的菜多做了多少份). ...

  2. noip2015提高组初赛

    一.单项选择题(共15题,每题1.5分,共计22.5分:每题有且仅有一个正确选项) 线性表若采用链表存储结构,要求内存中可用存储单元地址( ). A. 必须连续 B. 部分地址必须连续 C. 一定不连 ...

  3. KingbaseES 支持列加密

    KINGBASE 列加密支持 sm4 和 rc4 加密算法,具体算法在 initdb 时指定,默认是 sm4.要使用列加密,必须 shared_preload_libraries = 'sysencr ...

  4. k8s 如何关联pvc到特定的pv

    可以使用对 pv 打 label 的方式,具体如下: 创建 pv,指定 label $ cat nfs-pv2.yaml apiVersion: v1 kind: PersistentVolume # ...

  5. Kibana:在Kibana 中定制 time picker 及 指标可视化显示格式

    文章转载自:https://blog.csdn.net/UbuntuTouch/article/details/107066779

  6. tomcat的catalina.out日志按自定义时间格式进行分割

    默认情况下,tomcat的catalina.out日志文件是没有像其它日志一样,按日期进行分割,而是全部输出全部写入到一个catalina.out,这样日积月累就会造成.out日志越来越大,给管理造成 ...

  7. Kibana:在Kibana中定制Regional Map

  8. Fluentd直接传输日志给Elasticsearch

    官方文档地址:https://docs.fluentd.org/output/elasticsearch td-agent的v3.0.1版本以后自带包含out_elasticsearch插件,不用再安 ...

  9. SQL的事务

    一.基本概念 事务是数据库区别于文件系统的重要特性之一,当有了事务,就可以让数据库始终保持一致性,同时可以通过事务的机制恢复到某个时间点,保证了提交到数据库的修改不会因为系统崩溃而丢失: 事务只是一个 ...

  10. 计算机三大硬件和操作系统以及python解释器

    今日分享内容概要 计算机五大组成部分详解 计算机三大核心硬件 操作系统 编程与编程语言 编程语言的发展历史 编程语言的分类 python解释器 python解释器多版本共存 分享详细 计算机五大组成部 ...