经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
splay + 垃圾回收 知识点与例题的简要讲解
来源:cnblogs  作者:Vect0r_lemon  时间:2023/10/25 10:12:52  对本文有异议

splay 简要讲解

前置芝士:普通二叉树

splay tree是一个越处理越灵活的数据结构,通过splay(伸展)操作,使整棵树的单次查询时间复杂度接近于O(log n),整棵树的高度也接近于log n

根据上面的这句话,很明显能看出splay与普通二叉树的区别

普通二叉树经过多次处理后,很容易退化成链,单次查询的复杂度直升O(n),对于处理大型数据来说,这是绝对不能允许的

OI和ACM界也经常会有数据能使一个普通二叉树快速退化成链

例:  1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9

 

很明显,对于大型数据的处理来说,普通二叉树已经满足不了了

 

下面,我将会以例图来讲解中序遍历

这张图是我偷的,哈哈

 

在这组数据中,数的根节点为6,整棵树的中序遍历为1,2,3,4,5,6,7,10,15,一个树的中序遍历为:左子树 + 根 + 右子树

很明显,这是递增的,当前这个splay树维护的是一个递增的序列

________________________________

操作

_______________

splay 有一个核心操作,就叫splay(伸展)

splay操作是依靠rotate去实现的

rotate(x) 点x向上翻转,分左旋和右旋,使其中序遍历不变,详情见代码

(关键是将这玩意得要图,要不不好讲清,如果代码看不懂的话,点这里,至OI wiki)

splay(x,k) 将点x翻转到点k的下方,每次添加一个数,就将其splay到根节点的下方(每次翻转的复杂度为log n),这样的话就能使整棵树的高度接近于log n

-------------------------------

 

在树中每个点的前继(succ)就是其在数组中的上一个点,后继(pred)就是下一个点,

 

kth 查询第k大的数,在树中每个点的前继就是其在数组中的上一个点,后继就是下一个点,所以在其中序遍历有序的前提下,递归判断每个子树的大小就行了

当然,p2042这道题的kth是第k个数,并不是第k大的数,因为这道题(p2042)维护的是一个区间

---------

build(or insert) 创建子树(或插入一个数)

如果我们要创建一段数字,插入到下标x的后面,想一下,是不是可以先在这组将要插入的数中建树,然后直接将这个树插到下标为x的右子树下,是不是就可以了?

同样,insert(x)也是同理,只不过是其子树的大小只有1

---------

detele操作,垃圾回收

每一次delete(x)后,tree[x]这个点为空,是不是浪费了?这很不符合我们环保的心理 (=_=) ,所以在每次我们delete操作后,将其节点下标保存下来,到build(or insert)的时候再使用,这样是不是就做到回收再利用了?

---------------

当我们想要处理区间 [x,y] 的时候,记住无论我们怎么splay,其中序遍历一定不变,再想想,在splay中一个点的前继和后继是什么

所以我们就可以现将x splay到根节点的下方,将y splay到x的下方,其 [x,y] 是不是就是点x及其右子树 和 点y及其左子树,然后就可以区间处理了

___________________________________________

 

记住,splay所维护的可以不是一个数组的val值,他可以是数组的下标

___________________________________________

一切尽在注释之中,这道题所维护的是每一个点的下标,并不是其点的val

例题:P2042 [NOI2005] 维护数列

 

  1. splay+垃圾回收
  2. #include"bits/stdc++.h"
  3. using namespace std;
  4. const int N = 500010,INF = 1e9;
  5. #define inl inline
  6. #define reg register
  7. //#define ll long long
  8. int n,m;
  9. struct node
  10. {
  11. int s[2],p,v;
  12. int rev,same,size,sum,ms,ls,rs;
  13. //翻转标记,相同标记,子树大小,区间和,最大子段和,最大前缀和,最大后缀和
  14. inl void init(int _v,int _p) //预处理
  15. {
  16. s[0] = s[1] = 0,p = _p,v = _v;
  17. rev = same = 0;
  18. size = 1,sum = ms =v;
  19. ls = rs = max(v,0);
  20. }
  21. }tr[N];
  22. int root;
  23. int nodes[N],tt;//垃圾回收数组,tt为其大小
  24. int w[N];
  25. inl void pushup(int x) //向上传递
  26. {
  27. auto &u = tr[x],&l = tr[u.s[0]],&r = tr[u.s[1]];
  28. u.size = l.size + r.size + 1;
  29. u.sum = l.sum + r.sum + u.v;
  30. u.ls = max(l.ls,l.sum + u.v + r.ls);
  31. u.rs = max(r.rs,r.sum + u.v + l.rs);
  32. u.ms = max(max(l.ms,r.ms),l.rs + u.v + r.ls);
  33.   //处理各个数据
    }
  34. inl void pushdown(int x) //向下传递
  35. {
  36. auto &u = tr[x],&l = tr[u.s[0]], &r = tr[u.s[1]];
  37. if(u.same) //若有相同标记,则翻转标记就并不需要
  38. {
  39. u.same = u.rev = 0;
  40. if(u.s[0]) l.same = 1,l.v = u.v,l.sum = l.v * l.size;
  41. if(u.s[1]) r.same = 1,r.v = u.v,r.sum = r.v * r.size;
         
  42. if(u.v > 0)
  43. {
  44. if(u.s[0]) l.ms = l.ls = l.rs = l.sum;
  45. if(u.s[1]) r.ms = r.ls = r.rs = r.sum;
  46. }else
  47. {
  48. if(u.s[0]) l.ms = l.v,l.ls = l.rs = 0;
  49. if(u.s[1]) r.ms = r.v,r.ls = r.rs = 0;
  50. }
         //左右子树数据处理
  51. }else
  52. if(u.rev)
  53. {
  54. u.rev = 0,l.rev ^= 1,r.rev ^= 1;
  55. swap(l.ls,l.rs),swap(r.ls,r.rs);
  56. swap(l.s[0],l.s[1]),swap(r.s[0],r.s[1]);
         //翻转
  57. }
  58. }
  59. inl void rotate(int x)
  60. {
  61. int y = tr[x].p,z = tr[y].p;
  62. int k = tr[y].s[1] == x;
  63. tr[z].s[tr[z].s[1] == y] = x,tr[x].p = z;
  64. tr[y].s[k] = tr[x].s[k ^ 1],tr[tr[x].s[k ^ 1]].p = y;
  65. tr[x].s[k ^ 1] = y,tr[y].p = x;
  66. pushup(y),pushup(x);
      //向上旋转
  67. }
  68. inl void splay(int x,int k)
  69. {
  70. while (tr[x].p != k)
  71. {
  72. int y = tr[x].p, z = tr[y].p;
  73. if (z != k)
  74. if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
  75. else rotate(y);
  76. rotate(x);
  77. }
  78. if (!k) root = x;
  79. }
    //伸展
  80. inl int get_k(int k) //查询第k个数
  81. {
  82. int u = root;
  83. while (u)
  84. {
  85. pushdown(u);
  86. if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
  87. else if (tr[tr[u].s[0]].size + 1 == k) return u;
  88. else k -= tr[tr[u].s[0]].size + 1, u = tr[u].s[1];
  89. }
  90. }
  91. inl int build(int l,int r,int p) //建造子树
  92. {
  93. int mid = l + r >> 1;
  94. int u = nodes[tt -- ];
  95. tr[u].init(w[mid], p);
  96. if (l < mid) tr[u].s[0] = build(l, mid - 1, u);
  97. if (mid < r) tr[u].s[1] = build(mid + 1, r, u);
  98. pushup(u);
  99. return u;
  100. }
  101. inl void dfs(int u) //delete 操作
  102. {
  103. if(tr[u].s[0]) dfs(tr[u].s[0]);
  104. if(tr[u].s[1]) dfs(tr[u].s[1]);
  105. nodes[ ++ tt] = u; //垃圾回收
      
  106. }
  107. int main(void)
  108. {
  109. for(int i = 1;i < N;i ++) nodes[ ++ tt] = i;
  110. //nit [1,n] -> 垃圾回收站
  111. scanf("%d%d",&n,&m);
  112. tr[0].ms = -INF;
  113. w[0] = w[n+1] = -INF;
  114. for(int i = 1;i <= n;i ++) scanf("%d",&w[i]);
  115. root = build(0,n + 1,0);
  116. char op[20];
  117. while (m -- )
  118. {
  119. scanf("%s", op);
  120. if (!strcmp(op, "INSERT"))
  121. {
  122. int posi, tot;
  123. scanf("%d%d", &posi, &tot);
  124. for (int i = 0; i < tot; i ++ ) scanf("%d", &w[i]);
  125. int l = get_k(posi + 1), r = get_k(posi + 2);
  126. splay(l, 0), splay(r, l);
  127. int u = build(0, tot - 1, r);
  128. tr[r].s[0] = u;
  129. pushup(r), pushup(l);
  130. }
  131. else if (!strcmp(op, "DELETE"))
  132. {
  133. int posi, tot;
  134. scanf("%d%d", &posi, &tot);
  135. int l = get_k(posi), r = get_k(posi + tot + 1);
  136. splay(l, 0), splay(r, l);
  137. dfs(tr[r].s[0]);
  138. tr[r].s[0] = 0;
  139. pushup(r), pushup(l);
  140. }
  141. else if (!strcmp(op, "MAKE-SAME"))
  142. {
  143. int posi, tot, c;
  144. scanf("%d%d%d", &posi, &tot, &c);
  145. int l = get_k(posi), r = get_k(posi + tot + 1);
  146. splay(l, 0), splay(r, l);
  147. auto& son = tr[tr[r].s[0]];
  148. son.same = 1, son.v = c, son.sum = c * son.size;
  149. if (c > 0) son.ms = son.ls = son.rs = son.sum;
  150. else son.ms = c, son.ls = son.rs = 0;
  151. pushup(r), pushup(l);
  152. }
  153. else if (!strcmp(op, "REVERSE"))
  154. {
  155. int posi, tot;
  156. scanf("%d%d", &posi, &tot);
  157. int l = get_k(posi), r = get_k(posi + tot + 1);
  158. splay(l, 0), splay(r, l);
  159. auto& son = tr[tr[r].s[0]];
  160. son.rev ^= 1;
  161. swap(son.ls, son.rs);
  162. swap(son.s[0], son.s[1]);
  163. pushup(r), pushup(l);
  164. }
  165. else if (!strcmp(op, "GET-SUM"))
  166. {
  167. int posi, tot;
  168. scanf("%d%d", &posi, &tot);
  169. int l = get_k(posi), r = get_k(posi + tot + 1);
  170. splay(l, 0), splay(r, l);
  171. printf("%d\n", tr[tr[r].s[0]].sum);
  172. }
  173. else printf("%d\n", tr[root].ms); //MAX-SUM
  174. }
  175. return 0;
  176. }
  177. /*
    输入样例 1
  178. 9 8
  179. 2 -6 3 5 1 -5 -3 6 3
  180. GET-SUM 5 4
  181. MAX-SUM
  182. INSERT 8 3 -5 7 2
  183. DELETE 12 1
  184. MAKE-SAME 3 3 2
  185. REVERSE 3 6
  186. GET-SUM 5 4
  187. MAX-SUM
  188. */

可能对于刚刚学习splay的人来说,这道题明显有点难了

点这里,来做P3224 [HNOI2012] 永无乡

 答案我放在这里了,这道题维护的是数字的val

 这道题用了并查集的启发式合并,说白了就是在合并操作中将较小的集合合并到较大的集合中,以减少所浪费的时间

为什么要用启发式合并?合并操作不是O(1)吗?

不不不,这道题是并查集维护每个splay,splay的合并是O(log n),并不是O(1),需要启发式合并,来过毒瘤数据

  1. #include"bits/stdc++.h"
  2. using namespace std;
  3. const int N = 500010;
  4. #define inl inline
  5. #define reg register
  6. //#define ll long long
  7. int n,m;
  8. struct node
  9. {
  10. int s[2],p,v,id;
  11. int size;
  12. inl void init(int _v,int _id, int _p)
  13. {
  14. v = _v,id = _id,p = _p;
  15. size = 1;
  16. }
  17. }tr[N];
  18. int root[N],idx;
  19. int p[N];
  20. inl void read()
  21. {
  22. std::cin.tie(nullptr);
  23. }
  24. inl int Find(int x)
  25. {
  26. return p[x] != x ? p[x] = Find(p[x]) : p[x];
  27. }
  28. inl void pushup(int x)
  29. {
  30. tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + 1;
  31. }
  32. inl void rotate(int x)
  33. { //y为x的父节点,z为x的爷节点
  34. int y = tr[x].p,z = tr[y].p;
  35. int k = tr[y].s[1] == x;//k为(x是否为y的右节点)//即k为(x节点的左右儿子)
  36. tr[z].s[tr[z].s[1] == y] = x,tr[x].p = z;
  37. //z节点的y儿子改为x儿子,x节点的父节点改为z
  38. tr[y].s[k] = tr[x].s[k ^ 1],tr[tr[x].s[k ^ 1]].p = y;
  39. //处理原x的右儿子,以及原x右节点的parent
  40. tr[x].s[k ^ 1] = y,tr[y].p = x;
  41. //更改原x的非y儿子节点(另一个儿子),更改y的父节点为x
  42. pushup(y),pushup(x);
  43. //update size
  44. //x上移1个单位
  45. }
  46. inl void splay(int x,int k,int b)//将集合b的splay中x节点转到k节点的下面while(tr[x].p != k)
  47. {
  48. int y = tr[x].p, z = tr[y].p;
  49. if(z != k)
  50. if((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
  51. else rotate(y);
  52. //双旋
  53. rotate(x);
  54. }
  55. //k==0
  56. if(!k) root[b] = x;//集合b的根节点为x
  57. }
  58. inl void insert(int v,int id,int b)//将编号为id,重要度为v 的节点加入集合b的splay中
  59. {
  60. reg int u = root[b],p = 0;
  61. while(u) p = u,u = tr[u].s[v > tr[u].v];
  62. u = ++idx;
  63. if(p) tr[p].s[v > tr[p].v] = u;
  64. tr[u].init(v,id,p);
  65. splay(u,0,b);
  66. }
  67. inl void dfs(int u,int b)//将根节点u所在splay中的每一个点插入到集合b的splay中
  68. {
  69. if(tr[u].s[0]) dfs(tr[u].s[0],b);
  70. if(tr[u].s[1]) dfs(tr[u].s[1],b);
  71. insert(tr[u].v,tr[u].id,b);//查询集合b的splay中重要度第k小的节点编号
  72. }
  73. inl int get_k(int k,int b)//2é?ˉ?ˉo?bμ?Splay?D??òa?èμúkD?μ??úμ?±ào?
  74. {
  75. int u = root[b];
  76. while(u)
  77. {
  78. if(tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
  79. else if(tr[tr[u].s[0]].size + 1 == k) return tr[u].id;
  80. else k -= tr[tr[u].s[0]].size + 1,u = tr[u].s[1];
  81. }
  82. return -1;
  83. }
  84. int main(void)
  85. {
  86. read();
  87. scanf("%d%d",&n,&m);
  88. //init Find-Union and root
  89. for(int i = 1;i <= n;i ++)
  90. {
  91. p[i] = root[i] = i;
  92. int v;
  93. scanf("%d",&v);
  94. tr[i].init(v,i,0);/init 每个集合的root
  95. }
  96. idx = n;//前n个下标个n个splay的根节点用
  97. while(m--)
  98. {
  99. int a,b;
  100. scanf("%d%d",&a,&b);
  101. a = Find(a),b = Find(b);
  102. if(a != b)//如果两个岛不在一个集合中,需要合并
  103. {
  104. /*
  105. tr[root[a]].size > tr[root[b]].size
    保证集合a中元素的个数 <= 集合b中元素的个数
    启发式合并
    保证较小的集合加入较大的集合
  106. */
  107. if(tr[root[a]].size > tr[root[b]].size)
  108. swap(a,b);//保证集合a中的元素个数 <= 集合b中的元素个数
  109. dfs(root[a],b);//将集合a的splay里的每一个点插入到集合b的splay里面
  110. p[a] = b;//将集合a合并入集合b
  111. }
  112. }
  113. reg int q;
  114. scanf("%d",&q);
  115. while(q --)
  116. {
  117. char op[2];
  118. int a,b;
  119. scanf("%s%d%d",op,&a,&b);
  120. if(*op == 'B')//add edge
  121. {
  122. a = Find(a),b = Find(b);
  123. if(a != b)//Union 如果两个岛不在一个集合里,需要合并
  124. {
  125. if(tr[root[a]].size > tr[root[b]].size)
  126. swap(a,b);
  127. dfs(root[a],b);
  128. p[a] = b;
  129. }
  130. }else
  131. {
  132. a = Find(a);//集合中不足b个数,输出"-1"
  133. if(tr[root[a]].size < b) puts("-1");
  134. else printf("%d\n",get_k(b,a));//否则查询第k小数
  135. }
  136. }
  137. return 0;
  138. }

好了,就到这里了

数据结构不能单纯靠记忆,一定要理解,记住啊,一定要靠理解,记忆只不过是帮助你在考场上调试出来的东西,并不是其主导的因素

原文链接:https://www.cnblogs.com/better-coding/p/17774921.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号