Treap和Splay学习小结

来源:互联网 发布:mysql解压版配置 编辑:程序博客网 时间:2024/05/14 16:28

最近真的是花了很大的力气来学习平衡树的内容,因为之前在弄偏序的时候提到了平衡树的内容,真的是开了一个大坑啊。
先说说treap吧,网上的资料很多,描述的也很详细,我就不细说了,这里简单的谈谈我的看法,treap就是在二叉树的基础上加了一个随机域来保证堆的性质
这样可以把树的高度维护到期望的logn级别,其他的就是一棵二叉排序树,并没有什么特别的。
我实现了指针版和数组版两种,但是说实话,在写题的时候还是数组版的实用,无论是调试还是时间和代码量,数组都要显得优越很多。下面附上数组版的代码实现

struct Treaps{const static int maxn = 1e6 + 3e5;int L[maxn], R[maxn], v[maxn], p[maxn], A[maxn], C[maxn], tot;void clear(){ A[0] = L[0] = R[0] = C[0] = 0; tot = 1; }int Node(int V, int P){ L[tot] = R[tot] = 0; v[tot] = V; p[tot] = P; A[tot] = C[tot] = 1; return tot++; }void Count(int x){ C[x] = A[x] + C[L[x]] + C[R[x]]; }void rotate_right(int &x){int y = L[x]; L[x] = R[y]; R[y] = x; C[y] = C[x]; Count(x); x = y;}void rotate_left(int &x){int y = R[x]; R[x] = L[y]; L[y] = x; C[y] = C[x]; Count(x); x = y;}void Insert(int& x, int V, int P){if (!x) { x = Node(V, P); return; }if (v[x] == V) ++A[x];else if (V < v[x]){Insert(L[x], V, P);if (p[x]>p[L[x]]) rotate_right(x);}else{Insert(R[x], V, P);if (p[x] > p[R[x]]) rotate_left(x);}Count(x);}void add(int &x, int V){ Insert(x, V, rand()); }//外部直接调用,x是树根,v是值void Delete(int &x, int V){if (!x) return;if (V < v[x]) Delete(L[x], V);else if (V > v[x]) Delete(R[x], V);else if (A[x] > 1) --A[x];else if (!L[x] || !R[x]) x = L[x] + R[x];else if (p[L[x]] < p[R[x]]){ rotate_right(x); Delete(R[x], V); }else { rotate_left(x); Delete(L[x], V); }Count(x);}void dec(int &x, int V) { Delete(x, V); }//外部直接调用,x是树根,v是值int find(int x, int V)//返回树x中小于等于V的数字个数{int ans = 0;for (int i = x; i; i = V < v[i] ? L[i] : R[i]){if (v[i] <= V) ans += C[L[i]] + A[i];}return ans;}};

相比起treap,我觉得我对于splay的理解要深刻很多,毕竟做了那么多题,花了那么多时间的。
splay严格来讲不是二叉平衡树,但是它能保证均摊logn的效率,最神奇的是splay操作,简直就是线段树加强版。
相较于treap来说,splay不需要任何额外的内容,只要保证一个splay和旋转的操作即可,而所谓的splay操作
就是通过旋转把一个点向上转到目标点的操作。当然splay的具体操作等等都可以百度到,我就不多说了。
这里也谈谈我的理解吧,我觉得splay的关键操作只有两个,一个是旋转,一个是splay,下面附上代码


struct Splays{const static int maxn = 3e5 + 10;//节点个数const static int INF = 0x7FFFFFFF;//int最大值int ch[maxn][2], F[maxn], sz;//左右儿子,父亲节点和节点总个数int A[maxn];int Node(int f, int a) { A[sz] = a; ch[sz][0] = ch[sz][1] = 0; F[sz] = f; return sz++; }//申请一个新节点void clear(){ sz = 1; ch[0][0] = ch[0][1] = F[0] = 0; }//清空操作void rotate(int x, int k){int y = F[x]; ch[y][!k] = ch[x][k]; F[ch[x][k]] = y;if (F[y]) ch[F[y]][y == ch[F[y]][1]] = x;F[x] = F[y];    F[y] = x;ch[x][k] = y;//把y的值给x,重新计算y的值}void Splay(int x, int r){while (F[x]!=r){if (F[F[x]] == r) { rotate(x, x == ch[F[x]][0]); return; }int y = x == ch[F[x]][0], z = F[x] == ch[F[F[x]]][0];y^z ? (rotate(x, y), rotate(x, z)) : (rotate(F[x], z), rotate(x, y));}}void insert(int &x, int a){if (!x) { x = Node(0, a); return; }int now = 0;for (int i = x; i; i = ch[i][A[i] < a]){//下传延迟标记if (!ch[i][A[i] < a]) { now = ch[i][A[i] < a] = Node(i, a); break; }}Splay(now, 0);x = now;}}solve;

从代码上看是不是并没有比treap复杂,其实的确如此,并且splay可以做到区间反转,移动等等高难度动作,虽然treap也可强行的做,但是我觉得那样treap本身的随机域就失去了意义了。我觉得我独到的理解在于两处打了注释的地方,这是splay延迟标记的关键,就想线段树区间更新一样,我们只要在下去的时候传标记,上来的时候统计即可,
一开始我也是模仿他人的代码写的,但是呢我这个人有强波症,这个版本是我精简多次以后确认的,虽然可能不是最短的,但我觉得应该还是很清晰的,对于不同的题目来说
splay操作是无需改动的,插入操作可能不同,旋转操作只有注释的地方根据不同题目会有不同。

根据一个星期来的做题经验,splay的题目难点在于两个,一个是延迟标记的时候判断细节,一个是删除操作和合并操作的细节处理,这两个问题可以说是最为困难的,
往往是极小的一个细节错误就会导致tle啊wa啊什么的,感觉做splay的题目对于培养细心真的是很有帮助的。

推荐以下5道难题,搞定以后基本是splay大成了感觉。。

HYSBZ(BZOJ) 1500 维修数列
POJ 3580 SuperMemo
HDU 2475 Box

FZU 1978 Repair the brackets


HDU 3726 Graph and Queries


现在是1点半了,这几天为了搞定splay我也是疯了。。


2 0
原创粉丝点击