d0j1a_1701温馨提示:
这是一篇讲义 ,内容较优质,请放心食用。
前言在上一篇文章 中,我们已经学习了二叉查找树
和Treap
,现在我们来学习Splay
和无旋Treap
。
更多的平衡树 Splay访问局部性原理:
最近刚刚被访问过的节点极易被再次访问(时间局部性) 最近刚刚被访问过的节点的相邻节点极易被访问(空间局部性) Splay
(伸展树)是另一种平衡树实现。
由 Daniel Sleator 和 Robert Tarjan 发明 (怎么又是你) 。
Splay
根据访问局部性原理 设计,这种平衡树在访问节点后将它放在距离根节点较近的位置以增加查找速度。
与Treap
不同,Splay
不需要实时保持全树平衡。它只需要在每次访问一个节点后将它旋转到根节点并在沿途的旋转操作中调整树高。
splay操作splay
是Splay
树的一种特殊操作。(这句话区分大小写)
splay
操作通过一系列旋转将一个节点移动到平衡树的根节点(或其他节点)。
为了将节点移动到根节点,我们需要记录每个结点的父节点,这意味着需要重写节点定义和旋转操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct Node { int ch[2 ], fa, val, sz, cnt; } tree[100010 ];int root, len;inline int newNode (int val, int fa = 0 ) { return tree[++len] = {{0 , 0 }, fa, val, 1 , 1 }, len; }inline int getlr (int id) { return tree[tree[id].fa].ch[1 ] == id; }inline void pushup (int id) { tree[id].sz = tree[tree[id].ch[0 ]].sz + tree[tree[id].ch[1 ]].sz + tree[id].cnt; }inline void rotate (int id) { int fa = tree[id].fa, gf = tree[fa].fa, lr = getlr (id), ch = tree[id].ch[lr ^ 1 ]; tree[fa].ch[lr] = ch, tree[ch].fa = fa; tree[gf].ch[getlr (fa)] = id, tree[id].fa = gf; tree[id].ch[lr ^ 1 ] = fa, tree[fa].fa = id; pushup (fa), pushup (id); }
逐层伸展(spaly)有了旋转操作,splay
怎么实现就一目了然了。
于是我们可以很容易的写出splay
操作的代码:
1 2 3 4 5 inline void spaly (int id, int goal = 0 ) { while (tree[id].fa != goal) rotate (id); if (!goal) root = id; }
但这样做有一个致命的问题:可以被恶意构造的数据卡成 O ( n ) O(n) O ( n ) 。
如图:
可见只需要几个简单的单调数据就可以把平衡树卡成一叉查找链。
双层伸展(splay)为了解决这个致命的问题,我们引入双层伸展。
根据平衡树的结构,大致可将splay
操作需要处理的类型分为两种。
zigzig/zagzag型
当祖父节点,父亲节点与当前节点在同一条直线上时,先旋转父亲节点再旋转当前节点。
zigzag/zagzig型
当祖父节点,父亲节点与当前节点不在同一条直线上时,直接按照逐层伸展的方式处理即可。
这样即可得到双层伸展的splay
操作:
1 2 3 4 5 6 7 8 9 10 11 inline void splay (int id, int goal = 0 ) { while (tree[id].fa != goal) { int fa = tree[id].fa, gf = tree[fa].fa; if (gf != goal) { if (getlr (id) == getlr (fa)) rotate (fa); else rotate (id); } rotate (id); } if (!goal) root = id; }
对比一下逐层伸展和双层伸展:
可见双层伸展每次可让树高大致减少一半。
因此当毒瘤出题人试图去访问一条长链时,每次访问都会让树高减少一半。
虽然每次操作时间复杂度最差为 O ( n ) O(n) O ( n ) ,但Splay
树的均摊复杂度为 O ( n ) + O ( n / 2 ) + O ( n / 4 ) + O ( n / 8 ) + . . . + O ( 1 ) = O ( n log n ) O(n) + O(n/2) + O(n/4) + O(n/8) + ... + O(1) = O(n \log n) O ( n ) + O ( n / 2 ) + O ( n / 4 ) + O ( n / 8 ) + . . . + O ( 1 ) = O ( n log n ) 。
查找由于在Splay
中查找后会导致被查找结点旋转到根节点,所以Splay
中查找是一个很有用的操作。
这个函数查找的是小于等于val
的最大节点,如果val
在平衡树中会被旋转到根节点,否则将val
的前驱旋转到根节点。
1 2 3 4 5 6 7 int find (int val) { if (!root) return -1 ; int p = root; while (tree[p].ch[tree[p].val < val] && tree[p].val != val) p = tree[p].ch[tree[p].val < val]; splay (p); return p; }
前驱/后继有了find
函数后就十分容易实现。
查找前驱只需要find(x)
后在根节点的左子树上找最大值(如果根节点就是x
的前驱直接返回就好,否则左子树的最大值就是小于x
的最大值,即为前驱)。
查找后继同理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 inline int pre (int x) { find (x); if (tree[root].val < x) return root; int id = tree[root].ch[0 ]; while (tree[id].ch[1 ]) id = tree[id].ch[1 ]; splay (id); return id; }inline int nxt (int x) { find (x); if (tree[root].val > x) return root; int id = tree[root].ch[1 ]; while (tree[id].ch[0 ]) id = tree[id].ch[0 ]; splay (id); return id; }
查排名find(val)
后全部比val
小的节点都会在根节点的左子树上,所以只需要输出根节点左子树的大小+1即可。
1 2 3 4 inline int rnk (int val) { find (val); return tree[tree[root].ch[0 ]].sz + 1 ; }
删除显然,一个数的前驱和后继之间只可能有它自己。
1 2 3 4 5 6 7 8 9 void erase (int val) { int p = pre (val), q = nxt (val); splay (p), splay (q, p); int id = tree[q].ch[0 ]; if (tree[id].cnt > 1 ) tree[id].cnt--,tree[id].sz--, splay (id); else tree[q].ch[0 ] = 0 ; pushup (q); pushup (p); }
但是这样删除会出现问题:如果val
的前驱或后继不存在整棵树会爆炸(把一个不存在的节点转到根节点?)。
因此需要在建树时插入两个值为 i n f inf i n f 和 − i n f -inf − i n f 的哨兵节点。相应的,kth
操作和rnk
操作也需要特殊的处理。
Splay
树的其他操作和其他平衡树一致,只是别忘了在访问节点后进行splay
操作。
给出普通平衡树的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 #include <iostream> using namespace std;const int inf = 2147483647 ;struct Node { int ch[2 ], fa, val, sz, cnt; } tree[100010 ];int root, len;inline int newNode (int val, int fa = 0 ) { return tree[++len] = {{0 , 0 }, fa, val, 1 , 1 }, len; }inline int getlr (int id) { return tree[tree[id].fa].ch[1 ] == id; }inline void pushup (int id) { tree[id].sz = tree[tree[id].ch[0 ]].sz + tree[tree[id].ch[1 ]].sz + tree[id].cnt; }inline void rotate (int id) { int fa = tree[id].fa, gf = tree[fa].fa, lr = getlr (id), ch = tree[id].ch[lr ^ 1 ]; tree[fa].ch[lr] = ch, tree[ch].fa = fa; tree[gf].ch[getlr (fa)] = id, tree[id].fa = gf; tree[id].ch[lr ^ 1 ] = fa, tree[fa].fa = id; pushup (fa), pushup (id); }inline void splay (int id, int goal = 0 ) { while (tree[id].fa != goal) { int fa = tree[id].fa, gf = tree[fa].fa; if (gf != goal) { if (getlr (id) == getlr (fa)) rotate (fa); else rotate (id); } rotate (id); } if (!goal) root = id; }int find (int val) { if (!root) return -1 ; int p = root; while (tree[p].ch[tree[p].val < val] && tree[p].val != val) p = tree[p].ch[tree[p].val < val]; splay (p); return p; }void insert (int &id, int fa, int val) { if (!id) { splay (id = newNode (val, fa)); return ; } if (tree[id].val == val) tree[id].cnt++, splay (id); else insert (tree[id].ch[tree[id].val < val], id, val); }inline int rnk (int val) { find (val); return tree[tree[root].ch[0 ]].sz + 1 ; }int kth (int id, int k) { if (!id) return -1 ; if (k <= tree[tree[id].ch[0 ]].sz) return kth (tree[id].ch[0 ], k); else if (k <= tree[tree[id].ch[0 ]].sz + tree[id].cnt) return splay (id), tree[id].val; return kth (tree[id].ch[1 ], k - tree[id].sz + tree[tree[id].ch[1 ]].sz); }inline int pre (int x) { find (x); if (tree[root].val < x) return root; int id = tree[root].ch[0 ]; while (tree[id].ch[1 ]) id = tree[id].ch[1 ]; splay (id); return id; }inline int nxt (int x) { find (x); if (tree[root].val > x) return root; int id = tree[root].ch[1 ]; while (tree[id].ch[0 ]) id = tree[id].ch[0 ]; splay (id); return id; }void erase (int val) { int p = pre (val), q = nxt (val); splay (p), splay (q, p); int id = tree[q].ch[0 ]; if (tree[id].cnt > 1 ) tree[id].cnt--, tree[id].sz--, splay (id); else tree[q].ch[0 ] = 0 ; pushup (q); pushup (p); }int q;int main () { insert (root, 0 , inf), insert (root, 0 , -inf); cin >> q; while (q--) { int op, x; cin >> op >> x; switch (op) { case 1 : insert (root, 0 , x); break ; case 2 : erase (x); break ; case 3 : cout << rnk (x) - 1 << endl; break ; case 4 : cout << kth (root, x + 1 ) << endl; break ; case 5 : cout << tree[pre (x)].val << endl; break ; case 6 : cout << tree[nxt (x)].val << endl; break ; } } system ("pause" ); return 0 ; }
热知识:使用逐层伸展(即"单旋",同样的,双层伸展称为"双旋")的Splay
树一般被称为Spaly
。这个名字来源于luoguP3721 单旋 。
在随机数据下单旋Splay
的常数比双旋Splay
小。(普通平衡树中无论吸不吸O2
单旋都比双旋快 50 − 70 m s 50-70ms 5 0 − 7 0 m s )
无旋Treap以上提到的所有平衡树都基于旋转操作维持平衡。有没有不需要旋转的平衡树呢?
无旋Treap (又名FHQ-Treap
,因为发明者叫FHQ
)是一种无需旋转即可实现的平衡树,它同时具有支持区间操作 ,可持久化 ,码量小 的特点。
正如它的名字,无旋Treap
首先是棵Treap
。它的每个节点有val
数据域和weight
权值域。
不同于其他平衡树,无旋Treap
的基本操作只有**分裂(Split
)和 合并(Merge
)**操作。
分裂
这张图中的 k = 3 k=3 k = 3 应为 k = 2 k=2 k = 2 。(捂脸.jpg)
Split
操作可将一棵Treap
分裂为两棵Treap
,一棵的全部节点 v a l ≤ k val\le k v a l ≤ k ,另一棵全部节点 v a l > k val>k v a l > k 。
有一种简单的算法可以在 O ( log n ) O(\log n) O ( log n ) 的时间复杂度内进行分裂操作,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void split (int id,int k,int &x,int &y) { if (!id) { x = y = 0 ; return ; } if (val[id]<=k) { x = id; split (rc[id],k,rc[id],y); } else { y = id; split (lc[id],k,x,lc[id]); } updateSize (id); }
合并应该很好理解就不放动图了 实际上是我懒得做了
Merge
操作可将两棵Treap
合并为一棵Treap
,需要满足第一棵Treap
上全部节点小于等于第二棵树。
由于Treap
是一个堆,merge
时要将两个子树中weight
较大的放在上面。
同样也可以在 O ( log n ) O(\log n) O ( log n ) 时间复杂度内进行,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 int merge (int x,int y) { if (!x||!y) return x + y; if (w[x]>w[y]) { rc[x] = merge (rc[x],y); updateSize (x); return x; } else { lc[y] = merge (x,lc[y]); updateSize (y); return y; } }
插入这个时候,一定有人会说:
只会分裂和合并的平衡树我才不要!
但是有了这两个功能,就可以实现所有其他功能,并且代码量极少。
通过split
操作可以将一个Treap
分裂为两棵(分别是a:
v a l ≤ x val \le x v a l ≤ x ,b:
v a l > x val > x v a l > x ),我们只需要把一个新节点合并到a
里面,再给两棵树合并到一起就完成了Insert
操作。
1 2 3 4 5 void insert (int v) { int x,y; split (root,v,x,y); root = merge (merge (x,newNode (v)),y); }
只用五行代码就实现了插入操作!
删除只需要将一棵Treap
分裂为三棵(分别是a:
v a l < x val < x v a l < x ,b:
v a l = x val = x v a l = x 与 c:
v a l > x val > x v a l > x ),我们只需要把b
的两棵子树合并,再给两棵树合并到一起就完成了Insert
操作。
1 2 3 4 5 void erase (int v) { int x,y,z; split (root,v,x,z),split (x,v-1 ,x,y); root = merge (merge (x,merge (lc[y],rc[y])),z); }
排名rnk
操作也很简单,把一棵Treap
分裂成两棵(a:
v a l ≤ x val\le x v a l ≤ x 与 b:
v a l > x val>x v a l > x ),此时size[a]
就是比 x x x 小的节点个数,排名就是size[a]+1
。
1 2 3 4 5 6 7 int rnk (int v) { int x,y,res; split (root,v-1 ,x,y); res = sz[x] + 1 ; root = merge (x,y); return res; }
前驱/后继以前驱为例,所谓前驱就是比 x x x 小的数中最大的,只需要分裂成 a:
v a l < x val<x v a l < x 与 b:
v a l ≥ x val\ge x v a l ≥ x 两棵树然后找到a
上的最大值即可,后继同理。
如何找一棵Treap
上的最大值呢?只需要从根节点一直向右爬即可。找最小值就向左爬。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int pre (int v) { int x,y,p,res=-2147483647 ; split (root,v-1 ,x,y),p = x; while (p) res = val[p],p = rc[p]; root = merge (x,y); return res; }int nxt (int v) { int x,y,p,res=2147483647 ; split (root,v,x,y),p = y; while (p) res = val[p],p = lc[p]; root = merge (x,y); return res; }
其他操作别忘了无旋Treap
也是Treap
,直接复制即可。
普通平衡树我们再来切一下普通平衡树 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include <iostream> #include <random> using namespace std;mt19937 mt (random_device {}()) ;int root = 0 , len = 1 , val[100010 ], lc[100010 ], rc[100010 ], sz[100010 ], w[100010 ];inline int newNode (int v) { val[len] = v,sz[len] = 1 ,w[len] = mt (),lc[len] = rc[len] = 0 ; return len++; }inline void updateSize (int p) { sz[p] = sz[lc[p]] + sz[rc[p]] + 1 ; }void split (int id,int k,int &x,int &y) { if (!id) { x = y = 0 ; return ; } if (val[id]<=k) x = id,split (rc[id],k,rc[id],y); else y = id,split (lc[id],k,x,lc[id]); updateSize (id); }int merge (int x,int y) { if (!x||!y) return x + y; if (w[x]>w[y]) { rc[x] = merge (rc[x],y),updateSize (x); return x; } else { lc[y] = merge (x,lc[y]),updateSize (y); return y; } }void insert (int v) { int x,y; split (root,v,x,y); root = merge (merge (x,newNode (v)),y); }void erase (int v) { int x,y,z; split (root,v,x,z),split (x,v-1 ,x,y); root = merge (merge (x,merge (lc[y],rc[y])),z); }int rnk (int v) { int x,y,res; split (root,v-1 ,x,y); res = sz[x] + 1 ; root = merge (x,y); return res; }int kth (int k) { int p = root; while (1 ) { if (sz[lc[p]]+1 ==k) return val[p]; if (sz[lc[p]]>=k) p = lc[p]; else k -= sz[lc[p]] + 1 ,p = rc[p]; } return val[p]; }int pre (int v) { int x,y,p,res=-2147483647 ; split (root,v-1 ,x,y),p = x; while (p) res = val[p],p = rc[p]; root = merge (x,y); return res; }int nxt (int v) { int x,y,p,res=2147483647 ; split (root,v,x,y),p = y; while (p) res = val[p],p = lc[p]; root = merge (x,y); return res; }int main () { int n; cin >> n; while (n--) { int opt, x; cin >> opt >> x; switch (opt) { case 1 : insert (x); break ; case 2 : erase (x); break ; case 3 : cout << rnk (x) << endl; break ; case 4 : cout << kth (x) << endl; break ; case 5 : cout << pre (x) << endl; break ; case 6 : cout << nxt (x) << endl; break ; } } return 0 ; }
关于常数经过测试,普通平衡树中三种平衡树表现如下:
环境\时间\类型 Treap Splay 无旋Treap 洛谷 C++20 O2 397ms 454ms 482ms loj.ac C++20 O2 266ms 324ms 292ms
什么?你说O2 ?€$₱都开O2 了你确定你还不开?
多平台测试排除评测机波动等因素(loj.ac 的评测机真的快),可以发现Splay
和无旋Treap
的常数类似,但均比Treap
大。
原因很简单,Treap
的操作只需要从上到下执行一次,而Splay
需要在任意操作结束后进行splay
操作向上旋转一次,无旋Treap
需要多次split/merge
才能完成一次操作。
但在实际测试中,无旋Treap
的常数往往比Splay
小~~(玄学?)~~,并且编码复杂度比Splay
少上几倍,功能也丝毫不差,因此平常使用时一般使用无旋Treap
。
区间操作接下来到了重头戏——区间操作。本文关于区间操作的全部内容都使用无旋Treap
。(当然Splay
也可以实现,但它难写且常数比无旋Treap
大,因此如果你想了解Splay
区间操作请简单算法百度搜索深刻体会 )
支持区间操作的平衡树可以实现线段树的全部功能,甚至还能实现区间插入等线段树没有的功能。
我们使用luoguP3372 模板:线段树1 作为例题 (这是线段树的例题,所以我们不能使用线段树)
维护区间时,平衡树中维护的不是值,而是位置的大小关系 。
如果你没能理解这点你在学习后面内容时就会脑短路。
节点声明这道题要求你维护区间和,支持区间加。平衡树采用与线段树相同的方法实现区间操作:懒标记。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int root, len, lc[100010 ], rc[100010 ], sz[100010 ], w[100010 ];long long val[100010 ], sum[100010 ], add[100010 ];mt19937 mt (chrono::steady_clock::now().time_since_epoch().count()) ;inline int newNode (long long x) { ++len, val[len] = sum[len] = x, lc[len] = rc[len] = add[len] = 0 , sz[len] = 1 , w[len] = mt (); return len; }inline void pushup (int id) { sz[id] = sz[lc[id]] + sz[rc[id]] + 1 ; sum[id] = sum[lc[id]] + sum[rc[id]] + val[id]; }inline void pushdown (int id) { if (!add[id]) return ; if (lc[id]) val[lc[id]] += add[id], add[lc[id]] += add[id], sum[lc[id]] += sz[lc[id]] * add[id]; if (rc[id]) val[rc[id]] += add[id], add[rc[id]] += add[id], sum[rc[id]] += sz[rc[id]] * add[id]; add[id] = 0 ; }
初始化区间由于维护的是位置的大小关系,位置靠后的值一定大于位置靠前的值,我们只需要简单的把每一个值依次 merge
到无旋Treap
里即可。
这是merge
完成后Treap
可能的结构。
1 for (int i = 1 ; i <= n; i++) cin >> x, root = merge (root, newNode (x));
分裂区间为了更好的理解,在上一张图中我们把维护区间的平衡树视作以元素位置为关键字比较的平衡树。
但这个位置不一定要显式的表现(而且有的时候根本不能显式表现,比如需要进行动态插入或删除时)。观察这棵树可发现,每个元素在原数组中的位置就是它的排名rnk
。为了更方便的分裂出一个区间,我们可以修改split
函数为按排名分裂。
1 2 3 4 5 6 7 inline void split (int id, int k, int &x, int &y) { if (!id) return x = y = 0 , (void )0 ; pushdown (id); if (sz[lc[id]] + 1 <= k) x = id, split (rc[id], k - sz[lc[id]] - 1 , rc[id], y); else y = id, split (lc[id], k, x, lc[id]); pushup (id); }
这样,如果你要分裂出 [ l , r ] [l,r] [ l , r ] 这个区间,你只需要这样:
1 2 3 4 int a, b, c;split (root, r, b, c);split (b, l - 1 , a, b);
合并时直接正常合并即可,不需要做任何更改。
1 2 3 4 5 6 7 8 9 10 inline int merge (int x, int y) { if (!x || !y) return x + y; if (w[x] > w[y]) { pushdown (x), rc[x] = merge (rc[x], y), pushup (x); return x; } else { pushdown (y), lc[y] = merge (x, lc[y]), pushup (y); return y; } }
区间和只需要先分裂出这个区间,然后输出即可。
1 2 3 4 5 6 7 8 inline long long query (int l, int r) { long long ret = 0 ; int a, b, c; split (root, r, b, c), split (b, l - 1 , a, b); ret = sum[b]; root = merge (a, merge (b, c)); return ret; }
区间加先分裂出这个区间,再打懒标记(别忘了修改这个节点的值)。
1 2 3 4 5 6 inline void modify (int l, int r, long long x) { int a, b, c; split (root, r, b, c), split (b, l - 1 , a, b); add[b] += x, val[b] += x; root = merge (a, merge (b, c)); }
插入和删除这道题没有这两种操作,但是了解一下总是好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 inline void insert (int pos,int val) { int a, b; split (root, pos, a, b); root = merge (a, merge (newNode (val), b)); }inline void erase (int pos) { int a, b, c; split (root, pos-1 , a, b),split (b, 1 , b, c); root = merge (a, c); }inline void erase (int l, int r) { int a, b, c; split (root, r, b, c), split (b, l - 1 , a, b); root = merge (a, c); }
完整代码这样就做完了!
注意:这道题坑点很多,比如每个与值有关的变量都要long long
,返回值也要long long
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <iostream> #include <chrono> #include <random> using namespace std;int root, len, lc[100010 ], rc[100010 ], sz[100010 ], w[100010 ];long long val[100010 ], sum[100010 ], add[100010 ];mt19937 mt (chrono::steady_clock::now().time_since_epoch().count()) ;inline int newNode (long long x) { ++len, val[len] = sum[len] = x, lc[len] = rc[len] = add[len] = 0 , sz[len] = 1 , w[len] = mt (); return len; }inline void pushup (int id) { sz[id] = sz[lc[id]] + sz[rc[id]] + 1 ; sum[id] = sum[lc[id]] + sum[rc[id]] + val[id]; }inline void pushdown (int id) { if (!add[id]) return ; if (lc[id]) val[lc[id]] += add[id], add[lc[id]] += add[id], sum[lc[id]] += sz[lc[id]] * add[id]; if (rc[id]) val[rc[id]] += add[id], add[rc[id]] += add[id], sum[rc[id]] += sz[rc[id]] * add[id]; add[id] = 0 ; }inline void split (int id, int k, int &x, int &y) { if (!id) return x = y = 0 , (void )0 ; pushdown (id); if (sz[lc[id]] + 1 <= k) x = id, split (rc[id], k - sz[lc[id]] - 1 , rc[id], y); else y = id, split (lc[id], k, x, lc[id]); pushup (id); }inline int merge (int x, int y) { if (!x || !y) return x + y; if (w[x] > w[y]) { pushdown (x), rc[x] = merge (rc[x], y), pushup (x); return x; } else { pushdown (y), lc[y] = merge (x, lc[y]), pushup (y); return y; } }inline void modify (int l, int r, long long x) { int a, b, c; split (root, r, b, c), split (b, l - 1 , a, b); add[b] += x, val[b] += x; root = merge (a, merge (b, c)); }inline long long query (int l, int r) { long long ret = 0 ; int a, b, c; split (root, r, b, c), split (b, l - 1 , a, b); ret = sum[b]; root = merge (a, merge (b, c)); return ret; }int n, m, op, l, r;long long x;int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> x, root = merge (root, newNode (x)); while (m--) { cin >> op >> l >> r; if (op == 1 ) cin >> x, modify (l, r, x); else cout << query (l, r) << endl; } return 0 ; }
平衡树与线段树的优缺点平衡树的优点:极其易背(无旋Treap
只有两个关键函数,所有函数几乎都是对称的),可扩展性极强;
线段树的优点:效率相比平衡树较高(无旋Treap
开O2 896ms,线段树
不开O2 635ms);代码较短。