Splay伸展树入门(单点操作,区间维护)
Posted Q1143316492
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Splay伸展树入门(单点操作,区间维护)相关的知识,希望对你有一定的参考价值。
Pps:终于学会了伸展树的区间操作,做一个完整的总结,总结一下自己的伸展树的单点操作和区间维护,顺便给未来的自己总结复习用。
splay是一种平衡树,【平均】操作复杂度O(nlogn)。首先平衡树先是一颗二叉搜索树,刚刚开始学的时候找题hash数字的题先测板子。。。
后来那题被学长改了数据不能用平衡树测了。。。一道二分数字的题。
二叉搜索树的功能是,插入一个数字,在O(logn)的时间内找到它,并操作,插入删除等。但是可能会让二叉搜索树退化成链,复杂度达到O(n)
而平衡树就是通过一系列操作改变树的形态保存,尽可能把树的高度保持在logn,上图显然存在等价的树它不是一条链,
treap, avl, 红黑树,splay,替罪羊树。。。的各种巨巨发明的平衡方法,我选择了学splay,直接原因几个学长都用splay
而且splay相对红黑树等好写,能在ACM比赛的有限时间中改出来,而且能像线段树一样维护区间信息。
Splay不是严格平衡的,它是通过对每次操作的结点,操作后让它通过一系列旋转(zig, zag),伸展(splay)到根。80%的操作发生在20%的数据上,我们只要让它到根,尽快的取到就可以了。虽然它还是会退化成链,但是tarjan证明过它的均摊复杂度是O(nlogn)的。
我们先实现二叉搜索树的功能,存一个数字,判断它是否存在。
那么其实我们只要在插入数字,和查找数字后把那个结点旋转到根就满足了上面的要求。
先懂思路,大概文字能看的懂就好,代码实现可以慢慢继续往下看
其实旋转是无脑的。分为两种,右旋(zig),左旋(zag)。比如把x是它父节点的左儿子,那么它要往上走就要zig。反之就要zag
其中,x,y是节点,A,B,C可能是子树,也可以是空。左边到右边,旋转x,它是y的左儿子,就右旋转。右边到左边,y是x的右儿子就要左旋。
旋转方向的判断就判断是它父亲的左儿子还是右儿子即可。
仅仅上面的旋转是不够,Splay是需要双旋操作的。仅仅单旋的复杂度是O(n)的。
1,如果要旋转的结点x的父结点是根,那么只需要单旋。否则需要双旋转。
2,如果x和它的父节点y,同属于一种类型。这里的类型是指它们都是它父节点的右儿子,或者都是它父节点的左儿子。要先旋转y,再旋转x。
简单的解释就这样。复杂的细分就是其他巨巨博客说的zig-zig, zag-zag, zig - zag, zag-zig
静态splay,key是关键字,那里判断大小的值,cnt是key出现的次数。比如你插入两个3,只要cnt++就好了,不用插入两个值为3的结点。
father表示这个结点的父结点是谁,childs[2]表示它儿子结点是谁,_size表示这个结点的子树总共有多少结点。
静态链表建的树,root表示整颗splay的根,0一直是不用的根。sign表示我们用的空间从结构体sign+1取,而不用new出来。
重新设置结点x的结点大小,主要是在旋转,伸展的过程中需要一路update上去。
然后我的旋转函数,zig和zag可以合并。我注释那部分。像我这样比较菜的只会zig,zag分开写的顺一点。。。
我们用变量把x, y, z全记住,然后改指针就好了。拿zig为例。
第一行改变了x与B的关系。
第二行是x, y的关系,下图红线
接下来如果z存在。把x, z的关系改好。就完成了一次旋转rotate ,就和最终的右图一样了。具体x是z的左儿子还是右儿子要判断。
另外,可以注意到。rotate函数中我只update了y结点。理论上我改变了这么多结点,需要update的应该不止呀。这里其实是因为旋转完y是最低的结点。其他结点还需要继续splay。既然还需要改变,就不需要现在就急着splay。
把x结点旋转,goal是旋转后x的父节点。如果splay(x, 0)就是把x旋转到根。后面区间操作部分才需要修改goal.
if(goal == 0) root = x; 也是判断x是否旋转到根,没有旋转到根,根结点就不需要改变。在单点操作中if可以不需要直接root=x,但是区间操作中必须有。
然后我们插入数据。
然后插入一个权值为x的数字。就是普通二叉排序树的插入。注意在找到结点和没找到结点创建节点后。把该节点splay到根。
init函数请参考上文的结构体。
然后解决些问题,找根节点的前驱后继。小于根节点最大的数,和大于根节点最小的数。
找key为x的节点的排名
找排名为x的数
上面几个函数应该挺好看懂的,然后还有一个删除操作。splay的删除比较简单。他的思想是把待删除节点splay到root,删除根节点成为两颗子树,所以splay tree也有人叫分裂树。在把待删除旋转到根以后。
1,如果根节点的cnt>1,我们cnt--就结束了。
2,否则,原来的根节点必然是要丢掉的。在此条件下
1,如果左右子树都是空,那么整颗树也就空了,直接重置一颗空树。
2,左子树,或右子树右一个是空。如图,我们只要把root指向A, 原来的root丢掉,把A的father=0;
3,如果左右子树都存在。我们需要把root的前继旋转到根。因为是前继,所有结果会变成下图那样。root的左子树一点是空的。那么我们直接改变pre与root的右儿子的关系就好了。
例题:洛谷P3369 https://www.luogu.org/problemnew/show/P3369
参考代码:
1 #include <bits/stdc++.h> 2 3 using namespace std; 4 typedef long long LL; 5 const int maxn = 5e5 + 7; 6 7 struct Splay_Tree { 8 9 struct Node { 10 int father, childs[2], key, cnt, _size; 11 inline void init() { 12 father = childs[0] = childs[1] = key = cnt = _size = 0; 13 } 14 inline void init(int father, int lchild, int rchild, int key, int cnt, int sz) { 15 this -> father = father, childs[0] = lchild, childs[1] = rchild; 16 this -> key = key, this -> cnt = cnt, _size = sz; 17 } 18 } tre[maxn]; 19 int sign, root; 20 21 inline void init() { 22 sign = root = 0; 23 } 24 25 inline bool judge(int x) { 26 return tre[ tre[x].father ].childs[1] == x; 27 } 28 29 inline void update(int x) { 30 if(x) { 31 tre[x]._size = tre[x].cnt; 32 if(tre[x].childs[0]) { 33 tre[x]._size += tre[ tre[x].childs[0] ]._size; 34 } 35 if(tre[x].childs[1]) { 36 tre[x]._size += tre[ tre[x].childs[1] ]._size; 37 } 38 } 39 } 40 41 inline void rotate(int x) { 42 int y = tre[x].father, z = tre[y].father, k = judge(x); 43 44 //tre[y].childs[k] = tre[x].childs[!k], tre[ tre[x].childs[!k] ].father = y; 45 //tre[x].childs[!k] = y, tre[y].father = x; 46 //tre[z].childs[ tre[z].childs[1] == y ] = x, tre[x].father = z; 47 48 if(k == 0) { ///zig 49 tre[y].childs[0] = tre[x].childs[1], tre[ tre[x].childs[1] ].father = y; 50 tre[x].childs[1] = y, tre[y].father = x; 51 } else { ///zag 52 tre[y].childs[1] = tre[x].childs[0], tre[ tre[x].childs[0] ].father = y; 53 tre[x].childs[0] = y, tre[y].father = x; 54 } 55 tre[z].childs[ tre[z].childs[1] == y ] = x, tre[x].father = z; 56 57 update(y); 58 } 59 60 inline void splay(int x,int goal) { 61 for(int father; (father = tre[x].father) != goal; rotate(x) ) { 62 if(tre[father].father != goal) { 63 rotate(judge(x) == judge(father) ? father : x); 64 } 65 } 66 root = x; 67 } 68 69 inline void insert_node(int x) { 70 if(root == 0) { 71 tre[++sign].init(0, 0, 0, x, 1, 1); 72 root = sign; 73 return ; 74 } 75 int now = root, father = 0; 76 while(1) { 77 if(tre[now].key == x) { 78 tre[now].cnt ++; 79 update(now), update(father); 80 splay(now, 0); 81 break; 82 } 83 father = now; 84 if(x > tre[now].key) { 85 now = tre[now].childs[1]; 86 } else { 87 now = tre[now].childs[0]; 88 } 89 if(now == 0) { 90 tre[++sign].init(father, 0, 0, x, 1, 1); 91 if(x > tre[father].key) { 92 tre[father].childs[1] = sign; 93 } else { 94 tre[father].childs[0] = sign; 95 } 96 update(father); 97 splay(sign, 0); 98 break; 99 } 100 } 101 } 102 103 inline int pre() { 104 int now = tre[root].childs[0]; 105 while(tre[now].childs[1]) { 106 now = tre[now].childs[1]; 107 } 108 return now; 109 } 110 111 inline int next() { 112 int now = tre[root].childs[1]; 113 while(tre[now].childs[0]) { 114 now = tre[now].childs[0]; 115 } 116 return now; 117 } 118 119 inline int find_rank(int x) { /// 找x的排名 120 int now = root, ans = 0; 121 while(1) { 122 if(x < tre[now].key) { 123 now = tre[now].childs[0]; 124 } 125 else { 126 if(tre[now].childs[0]) { 127 ans += tre[ tre[now].childs[0] ]._size; 128 } 129 if(x == tre[now].key) { 130 splay(now, 0); 131 return ans + 1; 132 } 133 ans += tre[now].cnt; 134 now = tre[now].childs[1]; 135 } 136 } 137 } 138 139 inline int find_rankx(int x) { /// 找排名为x的数字 140 int now = root; 141 while(1) { 142 if(tre[now].childs[0] && x <= tre[ tre[now].childs[0] ]._size ) { 143 now = tre[now].childs[0]; 144 } else { 145 int lchild = tre[now].childs[0], sum = tre[now].cnt; 146 if(lchild) { 147 sum += tre[lchild]._size; 148 } 149 if(x <= sum) { 150 return tre[now].key; 151 } 152 x -= sum; 153 now = tre[now].childs[1]; 154 } 155 } 156 } 157 158 inline void del(int x) { 159 find_rank(x); 160 if(tre[root].cnt > 1) { 161 tre[root].cnt --; 162 update(root); 163 return ; 164 } 165 if(!tre[root].childs[0] && !tre[root].childs[1]) { 166 tre[root].init(); 167 root = 0; 168 return ; 169 } 170 if(!tre[root].childs[0]) { 171 int old_root = root; 172 root = tre[root].childs[1], tre[root].father = 0, tre[old_root].init(); 173 return ; 174 } 175 if(!tre[root].childs[1]) { 176 int old_root = root; 177 root = tre[root].childs[0], tre[root].father = 0, tre[old_root].init(); 178 return ; 179 } 180 int pre_node = pre(), old_root = root; 181 splay(pre_node, 0); 182 tre[root].childs[1] = tre[old_root].childs[1]; 183 tre[ tre[old_root].childs[1] ].father = root; 184 tre[old_root].init(); 185 update(root); 186 } 187 188 inline bool find(int x) { 189 int now = root; 190 while(1) { 191 if(now == 0) { 192 return 0; 193 } 194 if(x == tre[now].key) { 195 splay(now, 0); 196 return 1; 197 } 198 if(x > tre[now].key) { 199 now = tre[now].childs[1]; 200 } else { 201 now = tre[now].childs[0]; 202 } 203 } 204 } 205 206 } S; 207 208 int n, opt, x; 209 210 int main() { 211 while(~scanf("%d",&n)) { 212 S.init(); 213 for(int i = 1; i <= n; i ++ ) { 214 scanf("%d %d",&opt, &x); 215 switch(opt) { 216 case 1: 217 S.insert_node(x); 218 break; 219 case 2: 220 S.del(x); 221 break; 222 case 3: 223 printf("%d\\n",S.find_rank(x)); 224 break; 225 case 4: 226 printf("%d\\n",S.find_rankx(x)); 227 break; 228 case 5: 229 S.insert_node(x); 230 printf("%d\\n",S.tre[S.pre()].key); 231 S.del(x); 232 break; 233 case 6: 234 S.insert_node(x); 235 printf("%d\\n",S.tre[S.next()].key); 236 S.del(x); 237 break; 238 } 239 } 240 } 241 return 0; 242 }
splay的区间维护。
我们可以把下标作为key,插入平衡树。把L-1旋转到根,R+1旋转到根的右子树。根据平衡树的性质一点是这样的。这样区间[L,R]就到了根的右子树的左子树了。如图。
这样我们对区间的操作都可以写在节点上,还可以直接给节点打lazy标记。
我们需要给数组的n个元素开头来个-inf, 末尾来个inf,保持树的结构,然后类似线段树那样递归建树,直接就是平衡的。比较懒的一个一个insert也可以。
然后就是上面说的旋转L-1,R+1区间,然后给节点打标记了。
然后就是pushdown了,这部分初学的时候,我懵逼在了这里,尤其是为什么要swap左右节点的指针。貌似会改变平衡树的有序性。这里要自己想一想。
网上竟然没有一份代码解释了这里囧。。。
个人理解:首先我们找L,R的时候通过的是节点的size(详情看我的find函数),而不是直接找key为L的值,而key的cnt也就是节点位置重复元素都是1。我们确实不需要右儿子的key大于节点,左儿子的key小于这个性质。而我们在旋转L-1,R+1的节点后,[L,R]区间又体现为一个节点。每次都是同一个。
这里的pushdown只是延迟标记,我们要证明swap的正确性只要证明swap到底的正确性即可。
例题:洛谷P3391 只有区间翻转的例题 https://www.luogu.org/problemnew/show/P3391
参考代码:
1 #include <bits/stdc++.h> 2 3 using namespace std; 4 typedef long long LL; 5 const int maxn = 5e5 + 7; 6 const int inf = 1e9 + 7; 7 8 int n, m, arr[maxn]; 9 10 struct Splay_Tree { 11 12 struct Node { 13 int father, childs[2], key, cnt, _size, rev; 14 inline void init() { 15 father = childs[0] = childs[1] = key = cnt = _size = rev = 0; 16 } 17 inline void init(int father, int lchild, int rchild, int以上是关于Splay伸展树入门(单点操作,区间维护)的主要内容,如果未能解决你的问题,请参考以下文章