泗洪网站建设在线工具网站
splaysplaysplay 是一种平衡树,但是并不完全平衡。
它凭借一系列操作使得树趋近于平衡,所以与 AVLAVLAVL 比起来,时间可能没那么快,但是 splaysplaysplay 可以应用到的地方很多,比如说区间翻转,这是 AVLAVLAVL 无法做到的。
并且LCT要用到splay。
规定
- tree[x].son[0/1]tree[x].son[0/1]tree[x].son[0/1] 表示 xxx 的左右儿子是谁。
- tree[x].sizetree[x].sizetree[x].size 表示以 xxx 为根的子树大小。
- tree[x].fatree[x].fatree[x].fa 表示 xxx 的父亲节点。
原理
旋转
splaysplaysplay 也是利用旋转来维护的,它旋转时传的是儿子节点,然后将儿子节点转到父亲节点。
旋转时有两种情况,一种是我是你的左儿子,一种是我是你的右儿子,但其实两种情况分析起来本质上都差不多,可以打在一起。(不用像 AVLAVLAVL 一样分开)
代码
int pdd(int x) { return tree[tree[x].fa].son[1] == x; }//x是它父亲的哪个儿子
void rotato(int x) {int y = tree[x].fa, z = tree[y].fa, p = pdd(x);tree[z].son[pdd(y)] = x;tree[x].fa = z, tree[y].fa = x;tree[y].son[p] = tree[x].son[p ^ 1], tree[tree[x].son[p ^ 1]].fa = y;tree[x].son[p ^ 1] = y;updata(y), updata(x);//更新信息
}
splay
splaysplaysplay 肯定有 splaysplaysplay 操作。
splay(x,y)splay(x, y)splay(x,y) 就是用来将 xxx 转成 yyy 的儿子。
用到了旋转操作。
代码
bool pd(int x, int y) { return tree[x].fa != y; }
void splay(int x, int goal) {push(x);//因为有时候要区间翻转,这个用来用来下传翻转标记,如果题目没有翻转可以不用for (int y; pd(x, goal); rotato(x)) {y = tree[x].fa;if (pd(y, goal))//用来加快程序速度,删除对程序正确性不会有任何影响rotato(pdd(y) == pdd(x) ? y : x);}if (!goal)root = x;
}
查找,删除的操作都可以参照AVL来做,所以不再叙述。(可以看看我的另一篇博客)
翻转
我们来看看翻转操作如何实现。
我们假设要将区间 l,rl, rl,r 翻转,我们设 l′,r′l',r'l′,r′ 分别为 lll 位置的左边, rrr 位置的右边。
则我们可以先将 lll 节点 splaysplaysplay 到根节点,再将 rrr 节点 splaysplaysplay 到根节点的右儿子。
此时 rrr 节点的左子树就是我们要翻转的区间。
由于平衡树的性质,我们只需要将该子树的每个节点的左右儿子交换,便可翻转整个区间。
此时我们前面的 splaysplaysplay 中的 pushpushpush 就是用来将翻转标记翻转下来。
代码
void push(int x) {if (tree[x].fa)push(tree[x].fa);pushdown(x);
}
void reverse(int l, int r) { //此时的l,r就是l',r'splay(l, 0), splay(r, l), tree[tree[r].son[0]].flag ^= 1;
}
此时 splaysplaysplay 的基本操作就讲完了。