数据结构-伸展树(Splay)
伸展树,英文名叫 Splay Tree,是著名的 Tarjan 先生发明的一个神奇的数据结构。其本质是一棵二叉查找树,可以实现平衡树的所有操作,而且它的所有操作都基于伸展操作(将某个节点调整到根)。
另外提一嘴,这玩意叫 Splay,并不叫 Spaly。
基本操作
平衡树所有的操作 Splay 都可以做,而且在树上启发式合并的时候 Treap 甚至有时间复杂度上的优势。
旋转操作
与 treap 的旋转操作类似,这里将左旋和右旋合二为一了。先上一张经典的图:
我们把节点
代码:
int get(int x){return ch[fa[x]][1] == x;}
void rotate(int x) {
int y = fa[x], z = fa[y] , k = get(x);//判断
ch[y][k] = ch[x][k ^ 1];
if(ch[x][k ^ 1]) fa[ch[x][k ^ 1]] = y;// x 的某个子节点 & y
ch[x][k ^ 1] = y;
fa[y] = x;// x & y
if(z) ch[z][ch[z][1] == y] = x;
fa[x] = z;//x & z
push_up(y);push_up(x);
}
Splay操作
伸展树最核心的操作,其目的就是把节点
如果
接下来假设
zig-zig / zag-zag :如图所示,对应第
zig-zag/zag-zig:如图所示,对应第
这样子,我们每次可以将
代码:
void splay(int x) {
while(fa[x]){
if(fa[fa[x]]) rotate(get(x) == get(fa[x]) ? fa[x] : x);
rotate(x);
}
rt = x;
}
合并操作
这里的合并操作,要求伸展树
删除操作
首先在原树中找到
插入操作
在原树上找这个值,找得到就把这个值的次数加一,更新节点信息;找不到就新建一个节点。最后别忘了把这个点旋到根。
查询操作
求
求排名为
求某个数的前驱,就先在树中找到这个数,接着找到它左子树里最靠右边的节点。后继同理,在右子树里找最左边的节点。
当然,找到某个点后还是要通过把这个点旋到根节点的方式,来保证均摊的时间复杂度。
总结
其实还是挺好写的,但是在访问一个点后容易忘掉把它旋到根节点,修改一个点的值后容易忘记更新节点信息。
当然像刘昊,花花这种对璇那么上心的人肯定是不会忘的啦。
时间复杂度分析
这一部分其实才是伸展树算法的难点(但貌似不会也没有关系)。它采用了一个叫“势能分析” 的神奇分析方法,但众所周知,信息是一个看结果的学科,谁爱分析谁分析去吧。
定理:假设最多有
证明谁爱写谁写,反正不会没关系。有了这个定理,我们就可以证明对于所有操作,均摊都是
放一张 oi-wiki 的图上来,感兴趣的读者可以自行阅读。
#include <bits/stdc++.h>
using namespace std;
const int inf = 1e9 + 100;
const int N = 1e5 + 100;
int ch[N][2], fa[N];
int val[N], siz[N], cnt[N], idx;
int rt;
int New(int x){
val[++idx] = x;
siz[idx] = cnt[idx] = 1;
return idx;
}
int get(int x){return ch[fa[x]][1] == x;}
void push_up(int x){siz[x] = siz[ch[x][0]] + siz[ch[x][1]] + cnt[x];}
void rotate(int x) {
int y = fa[x], z = fa[y] , k = get(x);//判断
ch[y][k] = ch[x][k ^ 1];
if(ch[x][k ^ 1]) fa[ch[x][k ^ 1]] = y;// x 的某个子节点 & y
ch[x][k ^ 1] = y;
fa[y] = x;// x & y
if(z) ch[z][ch[z][1] == y] = x;
fa[x] = z;//x & z
push_up(y);push_up(x);
}
void splay(int x) {
while(fa[x]){
if(fa[fa[x]]) rotate(get(x) == get(fa[x]) ? fa[x] : x);
rotate(x);
}
rt = x;
fa[x] = 0;
push_up(x);
}
void init(){
New(-inf), New(inf);
rt = 1, ch[1][1] = 2, siz[1] = 2, fa[2] = 1;
}
int get_rank(int u, int k,int f){
if(!u){
splay(f);
return 0;
}
if(k == val[u]){
int res = siz[ch[u][0]] + cnt[u];
splay(u);
return res;
}
if(k < val[u]) return get_rank(ch[u][0], k, u);
int res = siz[ch[u][0]] + cnt[u];
return get_rank(ch[u][1], k, u) + res;
}
int get_num(int u, int k, int f){
if(!u){
splay(f);
return inf;
}
if(siz[ch[u][0]] >= k) return get_num(ch[u][0], k, u);
if(siz[ch[u][0]] + cnt[u] >= k){
splay(u);
return val[u];
}
return get_num(ch[u][1], k - siz[ch[u][0]] - cnt[u], u);
}
void ins(int u, int k , int f){
if(!u){
u = New(k);
fa[u] = f;
ch[f][k > val[f]] = u;
push_up(u);
push_up(f);
splay(u);
return ;
}
if(val[u] == k){
cnt[u]++;
push_up(u);
push_up(f);
splay(u);
return ;
}
ins(ch[u][k > val[u]], k, u);
}
int merge(int x, int y){
if(!x){
fa[y] = 0;
return y;
}
if(!y){
fa[x] = 0;
return x;
}
int t = x;
while(ch[t][1]) t = ch[t][1];
splay(t);
ch[t][1] = y;fa[t] = 0;
fa[y] = t;
return t;
}
void clear(int u){
fa[ch[u][0]] = fa[ch[u][1]] = 0;
cnt[u] = siz[u] = val[u] = ch[u][0] = ch[u][1] = 0;
}
void del(int u, int k, int f){
if(!u){
splay(f);
return ;
}
if(val[u] == k){
splay(u);
if(cnt[u] > 1) {
cnt[u]--;
push_up(u);
return ;
}
fa[ch[u][0]] = fa[ch[u][1]] = 0;
rt = merge(ch[u][0], ch[u][1]);
fa[u] = cnt[u] = siz[u] = val[u] = ch[u][0] = ch[u][1] = 0;
return ;
}
del(ch[u][k > val[u]], k, u);
push_up(u);
}
int get_pre(int k){
int ans = 1, u = rt;
while(u){
if(val[u] == k){
if(ch[u][0]){
u = ch[u][0];
while(ch[u][1]) u = ch[u][1];
ans = u;
}
break;
}
if(val[u] < k && val[u] > val[ans]) ans = u;
u = ch[u][k > val[u]];
}
splay(ans);
return val[ans];
}
int get_nxt(int k){
int ans = 2, u = rt;
while(u){
if(val[u] == k){
if(ch[u][1]){
u = ch[u][1];
while(ch[u][0]) u = ch[u][0];
ans = u;
}
break;
}
if(val[u] > k && val[u] < val[ans]) ans = u;
u = ch[u][k > val[u]];
}
splay(ans);
return val[ans];
}
int main(){
int n;cin >> n;
init();
for(int i = 1;i <= n;i++) {
int opt, x;
cin >> opt >> x;
if(opt == 1) ins(rt, x, 0);
if(opt == 2) del(rt, x, 0);
if(opt == 3) cout << get_rank(rt, x - 1, 0) << endl;
if(opt == 4) cout << get_num(rt, x + 1, 0) << endl;
if(opt == 5) cout << get_pre(x) << endl;
if(opt == 6) cout << get_nxt(x) << endl;
}
return 0;
}
树上启发式合并
(定理
问题:对于
定理1:正常平衡树可以在
证明1:每次合并整体分析比较难搞,不如考虑每个元素对于总时间复杂度的贡献。当
定理2:对于一个
这个我不会证。
定理2的引理: 对于两棵大小分别为
定理3 :Splay 可以在
证明3:我们将
所以总复杂度是
总而言之,当我们需要不断合并两个平衡树的时候,用伸展树来实现可以让时间复杂度更优。