【bzoj1251】序列终结者(伸展树)

Description

网上有许多题,就是给定一个序列,要你支持几种操作:A、B、C、D。一看另一道题,又是一个序列 要支持几种操作:D、C、B、A。尤其是我们这里的某人,出模拟试题,居然还出了一道这样的,真是没技术含量……这样 我也出一道题,我出这一道的目的是为了让大家以后做这种题目有一个“库”可以依靠,没有什么其他的意思。这道题目 就叫序列终结者吧。 【问题描述】 给定一个长度为N的序列,每个序列的元素是一个整数(废话)。要支持以下三种操作: 1. 将[L,R]这个区间内的所有数加上V。 2. 将[L,R]这个区间翻转,比如1 2 3 4变成4 3 2 1。 3. 求[L,R]这个区间中的最大值。 最开始所有元素都是0。

Input

第一行两个整数N,M。M为操作个数。 以下M行,每行最多四个整数,依次为K,L,R,V。K表示是第几种操作,如果不是第1种操作则K后面只有两个数。

Output

对于每个第3种操作,给出正确的回答。

Sample Input

4 4
1 1 3 2
1 2 4 -1
2 1 3
3 2 4

Sample Output

2
【数据范围】
N<=50000,M<=100000。

样例说明:

  0 0 0 0
1 1 3 2 2 2 2 0
1 2 4 -1 2 1 1 -1
2 1 3 1 1 2 -1
3 2 4 2

分析:

暴力的话,操作1增加操作,操作2翻转操作,操作3查询操作的复杂度都是O(n),并且有m个查询的话,O(mn)肯定得爆炸。

关键点:
1. 伸展树为左小右大的二叉树,所以旋转操作不会影响树的性质
2. 区间操作为:
int u = select(L - 1), v = select(R + 1);
splay(u, 0); splay(v, u);
//通过旋转操作把询问的区间聚集到根的右子树的左子树下
因为伸展树为左小右大的二叉树,旋转操作后的所以对于闭区间[L, R]之间的所有元素都聚集在根的右子树的左子树下
因为闭区间[L, R],
1) 所以每次都要查开区间(L - 1, R + 1),
2) 所以伸展树元素1对应的标号为2,
3) 所以node[0]对应空节点,node[1]对应比所以元素标号都小的点,node[2 ~ n + 1]对应元素1 ~ n,node[n + 2]对应比所有元素标号都打的点,其中node[0], node[1], node[n + 2]都是虚节点,不代表任何元素。

每次进行序列操作时,把l-1旋转到根,把r+1旋转到根的右儿子,r+1的左子树就是整个区间[l,r]

我们可以用Splay的每个节点记录该节点对应子树的信息,那么每次询问只要输出r+1的左子树中的最大值,即代码中的mx[t[y][0]]。

为了避免Splay中有节点0,我们将所有节点的编号加1。又因为要旋转l-1和r+1,所以在Splay插入节点为1到n+2。(原因显然…大家自己脑补)

这道题用Splay的提根操作达到了区间操作的目的,方法很巧妙。

另外我觉得这道题有几点需要注意:

①要理解Splay中节点的含义以及节点所记录的信息。

②区间的翻转操作很巧妙,只需要将标记下传并且交换左右子树,并不需要修改节点的max和size。

③每次find操作都要pushdown,这样就可以保证节点x到根的路径上所有点都被更新,便于之后的旋转操作

总结

a. 这里区间加,所以无怪乎有延迟标记的思想,那就自然有了pushdown操作

b. 这里通过提根操作找到我们要操作的区间,

c. 区间加操作用的是延迟标记的思想

b. 区间最大值操作在排序二叉树中很简单(因为这个区间已经被我们旋转成一个区间了)

d. 区间翻转操作:这里用的是延迟标记的思想(区间加也是延迟标记),所以有rev做延迟标记,交换的话直接交换左右即可

e. 这里有翻转操作,这颗伸展树不一定是一颗二叉排序树,所以求最大值的话就像线段树那么求好了,每个节点多加个maxx标记即可

  1. /*bzoj 1251 序列终结者
  2. 题意:
  3. 给定一个长度为N的序列,每个序列的元素是一个整数。要支持以下三种操作:
  4. 1. 将[L,R]这个区间内的所有数加上V;
  5. 2. 将[L,R]这个区间翻转,比如1 2 3 4变成4 3 2 1;
  6. 3. 求[L,R]这个区间中的最大值;
  7. 最开始所有元素都是0。
  8. 限制:
  9. N <= 50000, M <= 100000
  10. 思路:
  11. 伸展树
  12.  
  13. 关键点:
  14. 1. 伸展树为左小右大的二叉树,所以旋转操作不会影响树的性质
  15. 2. 区间操作为:
  16. int u = select(L - 1), v = select(R + 1);
  17. splay(u, 0); splay(v, u); //通过旋转操作把询问的区间聚集到根的右子树的左子树下
  18. 因为伸展树为左小右大的二叉树,旋转操作后的所以对于闭区间[L, R]之间的所有元素都聚集在根的右子树的左子树下
  19. 因为闭区间[L, R],
  20. 1) 所以每次都要查开区间(L - 1, R + 1),
  21. 2) 所以伸展树元素1对应的标号为2,
  22. 3) 所以node[0]对应空节点,node[1]对应比所以元素标号都小的点,node[2 ~ n + 1]对应元素1 ~ n,node[n + 2]对应比所有元素标号都打的点,其中node[0], node[1], node[n + 2]都是虚节点,不代表任何元素。
  23. */
  24. #include <iostream>
  25. #include <cstdio>
  26. using namespace std;
  27. //左右孩子简便写
  28. #define LS(n) node[(n)].ch[0]
  29. #define RS(n) node[(n)].ch[1]
  30.  
  31. const int N = 1e5 + ;
  32. const int INF = 0x3f3f3f3f;
  33. struct Splay {
  34. struct Node{
  35. int fa, ch[];//节点的父亲以及两个孩子
  36. bool rev;//翻转标记
  37. int val, add, maxx, size;//值,增加的延迟标记,最大值,子树的大小
  38. void init(int _val) {
  39. val = maxx = _val;//初始化最大值和值
  40. size = ;//子树大小
  41. add = rev = ch[] = ch[] = ;//初始化左右子树和延迟标记和反转标记
  42. }
  43. } node[N];//n个节点
  44. int root;//树根
  45.  
  46. void up(int n) {//右节点向父亲更新
  47. //这是求子树的最大值,树的最大值就是取树根,左子树,右子树三者中的最大值
  48. node[n].maxx = max(node[n].val, max(node[LS(n)].maxx, node[RS(n)].maxx));
  49. //这是更新树根的大小,左子树+右子树+根
  50. node[n].size = node[LS(n)].size + node[RS(n)].size + ;
  51. }
  52.  
  53. void down(int n) {//区间增加的延迟标记往下传的操作
  54. if(n == ) return ;//空节点
  55. if(node[n].add) {//如果增加的延迟标记不为0
  56. if(LS(n)) {//如果分别有左右子树,就更新左右子树
  57. //标准的线段树区间操作的例子
  58. node[LS(n)].val += node[n].add;
  59. node[LS(n)].maxx += node[n].add;
  60. node[LS(n)].add += node[n].add;
  61. }
  62. if(RS(n)) {
  63. node[RS(n)].val += node[n].add;
  64. node[RS(n)].maxx += node[n].add;
  65. node[RS(n)].add += node[n].add;
  66. }
  67. node[n].add = ;//增加延迟标记传下去了,自己的当然要赋值为0
  68. }
  69. if(node[n].rev) {//这是区间翻转的延迟标记
  70. if(LS(n)) node[LS(n)].rev ^= ;//翻转延迟标记往下传
  71. if(RS(n)) node[RS(n)].rev ^= ;
  72. swap(LS(n), RS(n));//交换左右子树
  73. node[n].rev = ;//翻转延迟标记设置为0
  74. }
  75. }
  76.  
  77. //左旋和右旋的合集 ,将节点n按照kind方式旋转
  78. void rotate(int n, bool kind) {
  79. int fn = node[n].fa;
  80. int ffn = node[fn].fa;
  81. node[fn].ch[!kind] = node[n].ch[kind];
  82. node[node[n].ch[kind]].fa = fn;
  83.  
  84. node[n].ch[kind] = fn;
  85. node[fn].fa = n;
  86.  
  87. node[ffn].ch[RS(ffn) == fn] = n;
  88. node[n].fa = ffn;
  89. up(fn);
  90. }
  91.  
  92. //将节点n伸展到goal的 位置去
  93. void splay(int n, int goal) {
  94. while(node[n].fa != goal) {
  95. int fn = node[n].fa;
  96. int ffn = node[fn].fa;
  97. down(ffn); down(fn); down(n);
  98. bool rotate_n = (LS(fn) == n);
  99. bool rotate_fn = (LS(ffn) == fn);
  100. if(ffn == goal) rotate(n, rotate_n);
  101. else {
  102. if(rotate_n == rotate_fn) rotate(fn, rotate_fn);
  103. else rotate(n, rotate_n);
  104. rotate(n, rotate_fn);
  105. }
  106. }
  107. up(n);
  108. if(goal == ) root = n;
  109. }
  110.  
  111. //在树种找位置为pos的点,其实和二叉查找树里面找排名为pos的点的方式一样
  112. int select(int pos) {
  113. int u = root;
  114. down(u);//区间加和区级翻转延迟标记下传
  115. while(node[LS(u)].size != pos) {//左孩子的大小不等于pos
  116. if(pos < node[LS(u)].size)//如果pos在左孩子就往左孩子走
  117. u = LS(u);//
  118. else {//pos在右孩子
  119. pos -= node[LS(u)].size + ;//pos减去左孩子和根的大小
  120. u = RS(u);//往右孩子走
  121. }
  122. down(u);//延迟标记下传
  123. }
  124. return u;
  125. }
  126.  
  127. //查找l到r这个区间
  128. int query(int L, int R) {
  129. //u节点就是找到的l-1的节点,v节点就是找到的r+1的节点
  130. int u = select(L - ), v = select(R + );
  131. //将u节点旋转到0的位置(根),将v节点旋转到u的位置,那么
  132. splay(u, ); splay(v, u); //通过旋转操作把询问的区间聚集到根的右子树的左子树下
  133. return node[LS(v)].maxx;
  134. }
  135.  
  136. //区间加操作
  137. void update(int L, int R, int val) {
  138. //把区间调上来
  139. int u = select(L - ), v = select(R + );
  140. splay(u, ); splay(v, u);
  141. //标准的区间加操作
  142. node[LS(v)].val += val;
  143. node[LS(v)].maxx += val;
  144. node[LS(v)].add += val;//延迟标记下传
  145. }
  146.  
  147. //区间翻转操作
  148. void reverse(int L, int R) {
  149. //找区间
  150. int u = select(L - ), v = select(R + );
  151. splay(u, ); splay(v, u);
  152. //翻转延迟标记置为1
  153. node[LS(v)].rev ^= ;
  154. }
  155.  
  156. int build(int L, int R) {
  157. if(L > R) return ;
  158. if(L == R) return L;
  159. int mid = (L + R) >> ;
  160. int r_L, r_R;
  161. LS(mid) = r_L = build(L, mid - );
  162. RS(mid) = r_R = build(mid + , R);
  163. node[r_L].fa = node[r_R].fa = mid;
  164. up(mid);
  165. return mid;
  166. }
  167.  
  168. void init(int n) {
  169. node[].init(-INF); node[].size = ;
  170. node[].init(-INF);
  171. node[n + ].init(-INF);
  172. for(int i = ; i <= n + ; ++i)
  173. node[i].init();
  174.  
  175. root = build(, n + );
  176. node[root].fa = ;
  177.  
  178. node[].fa = ;
  179. LS() = root;
  180. }
  181. } splay_tree;
  182.  
  183. int main() {
  184. int n, m;
  185. scanf("%d%d", &n, &m);
  186. splay_tree.init(n);//初始化
  187. for(int i = ; i < m; ++i) {
  188. int op, l, r, v;
  189. scanf("%d", &op);
  190. if(op == ) {//操作1,区间加
  191. scanf("%d%d%d", &l, &r, &v);
  192. splay_tree.update(l, r, v);//l到r区间上面加上v
  193. } else if(op == ) {//操作2,区间翻转
  194. scanf("%d%d", &l, &r);
  195. splay_tree.reverse(l, r);//翻转l到r区间
  196. } else {
  197. scanf("%d%d", &l, &r);
  198. printf("%d\n",splay_tree.query(l, r));//查询l到r的最大值
  199. }
  200. }
  201. return ;
  202. }

【bzoj1251】序列终结者(伸展树)的更多相关文章

  1. [BZOJ1251]序列终结者

    [BZOJ1251]序列终结者 试题描述 网上有许多题,就是给定一个序列,要你支持几种操作:A.B.C.D.一看另一道题,又是一个序列 要支持几种操作:D.C.B.A.尤其是我们这里的某人,出模拟试题 ...

  2. BZOJ1251序列终结者——非旋转treap

    题目描述 网上有许多题,就是给定一个序列,要你支持几种操作:A.B.C.D.一看另一道题,又是一个序列 要支持几种操作:D.C.B.A.尤其是我们这里的某人,出模拟试题,居然还出了一道这样的,真是没技 ...

  3. [bzoj1251]序列终结者_splay

    序列终结者 bzoj-1251 题目大意:给定一个长度为n的正整数序列,支持区间加,区间反转,查询区间最大值.所有元素开始都是0. 注释:$1\le n\le 5\cdot 10^4$,操作个数不多于 ...

  4. bzoj1251 序列终结者(Splay Tree+懒惰标记)

    Description 网上有许多题,就是给定一个序列,要你支持几种操作:A.B.C.D.一看另一道题,又是一个序列 要支持几种操作:D.C.B.A.尤其是我们这里的某人,出模拟试题,居然还出了一道这 ...

  5. [bzoj1251]序列终结者——splay

    题目大意 网上有许多题,就是给定一个序列,要你支持几种操作:A.B.C.D.一看另一道题,又是一个序列 要支持几种操作:D.C.B.A.尤其是我们这里的某人,出模拟试题,居然还出了一道这样的,真是没技 ...

  6. BZOJ1251 序列终结者(Splay平衡树)(占位)

    网上有许多题,就是给定一个序列,要你支持几种操作:A.B.C.D.一看另一道题,又是一个序列 要支持几种操作:D.C.B.A.尤其是我们这里的某人,出模拟试题,居然还出了一道这样的,真是没技术含量…… ...

  7. BZOJ1251——序列终结者

    给你一个数列,让你实现区间加上一个值,区间翻转,区间最大值 裸splay,懒标记一发即可 #include <cstdio> #include <cstdlib> #inclu ...

  8. bzoj1251 序列终结者(splay)

    人生第一发splay,写得巨丑,最后忘记了push_down以后要将子节点maintain 9k代码不忍直视 #define NDEBUG #include<cstdio> #includ ...

  9. 题解【bzoj1251 序列终结者】

    Description 维护三个操作:区间加,区间翻转,区间求最大值.\(n \leq 50000\) Solution fhqtreap大法好! 模板题(我是不会告诉你这篇题解是用来存个代码的 Co ...

  10. bzoj1251: 序列终结者 fhqtreap写法

    fhqtreap的速度果然很快 花了时间学了下指针写法 没有旋转 只有分裂以及合并操作 其实还是蛮好写的 #include<cstdio> #include<cstring> ...

随机推荐

  1. ThinkPHP---辅助方法

    [三]Tp常见的辅助方法 原生SQL语句里除了目前所使用的基本操作增删改查,还有类似于group.where.order.limit等这样的字句. ThinkPHP封装了相应的子句方法:封装的方法都在 ...

  2. JS数组——冒泡、插入、快速排序

    前言:因为要对后端返回来的数据进行处理,之前之后冒泡,不够用,去看了插入跟快速,写下这篇笔记. 使用背景: 1.冒泡排序 数据比较少,小于1000 2.插入排序 数据比较少,大于1000不推荐 3.快 ...

  3. 网络编程-socketserver

    网络编程使用socketserver,通常包括以下几步:一.定义类,并继承socketserver.BaseRequestHandler 二.重写handle方法 三.实例化TCPServer,并传递 ...

  4. The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [C:\Program Files\Java\jdk1.8.0_60\bin;C:\Windows\Sun\Jav

    启动项目自动结束,查看日志发现 [ost-startStop-1] o.a.catalina.core.AprLifecycleListener   : The APR based Apache To ...

  5. Xcode5编译ffmpeg

    命令行安装FFmpeg:git clone git://source.ffmpeg.org/ffmpeg.git ffmpeg(或:到https://github.com/gabriel/ffmpeg ...

  6. dorado 7 使用总结

    最近项目上需要,使用了dorado 7 ,总体感觉还可以,快速开发很方便,然而在方便的同时,难免有些太过繁琐,很多东西都封装了起来,会造成很多不便.因此快速开发的项目可以使用,其它的不推荐.现在打算将 ...

  7. 母牛的故事(hdoj 2018,动态规划递推,详解)

    有一头母牛,它每年年初生一头小母牛.每头小母牛从第四个年头开始,每年年初也生一头小母牛.请编程实现在第n年的时候,共有多少头母牛? Sample Input2450Sample Output246 / ...

  8. PKI相关知识简述

    1. 公钥泄露导致中间人攻击 有A.B.C三个人,如果C把自己的公钥提供给了AB双方,C伪装成B,让A认为C就B,这样A就把自己的公钥发送给C,C再伪装成A,让B认为C就A,B就把自己的公钥也发送给了 ...

  9. 在线安全清空慢查询日志slowlog

      mysql> show variables like '%slow_query%';+------------------------------------+--------------- ...

  10. 第一个Maven工程的目录结构和文件内容及联网问题

    [第一个Maven工程] ①目录结构 Hello |---src |---|---main |---|---|---java |---|---|---resources |---|---test |- ...