前言
splay 是一种强大的数据结构,是可以动态维护树的状态的数据结构。那么这种结构的主要操作就是 splay , splay 又是靠 rotate 实现的。所以 rotate 是最重要的。
那么先介绍
rotate
,这个
rotate
有许多种版本,根据我精心(为了少打码)研究,发现了一个很好的版本,这个版本,也使得其
splay
异常的美丽,这个版本是
Cloveunique
所写的。
传送门在此
我们经常看到标准的 splay 有左旋右旋之分,但是这个版本的则不用,因为 splay 的左右旋等的区分是看父亲,爷爷与它是不是一条链上,如果是,则先转父亲,反之就先转它,这样子,就可以很好的维护一下这个平衡树的性质,而不用莫名的一堆判断。(实际上指针版的 splay 更快,但是我太弱了,写不来啊。)
18.4.23
稍微补充一下指针版的splay以及其需要注意的部分。
#include <bits/stdc++.h>
#define ls ch[0]
#define rs ch[1]
using namespace std;
const int N = 1e5+10;
const int inf = 1e9;
struct Node {
Node *ch[2],*fa;
int cnt,sz,key;
Node() {}
Node(int key) :key(key) {
ls = rs = fa = NULL;
cnt = sz = 1;
}
void update() {
sz = cnt + (ls?ls->sz:0) + (rs?rs->sz:0);
}
}*root;
int get(Node *u){ return u->fa->ch[1] == u; }
void rotate(Node *u){
Node *old = u->fa , *oldfa = old->fa;
if ( oldfa )
oldfa -> ch[ oldfa->ch[1]==old ] = u;
u -> fa = oldfa;
int d = old -> ch[1] == u;
old -> ch[d] = u -> ch[d^1];
if (u -> ch[d^1] )u -> ch[d^1] -> fa = old;
u -> ch[d^1] = old;
old -> fa = u;
old->update(); u-> update();
}
void splay(Node *u){
for (Node *f ; f = u -> fa ; rotate(u) )
if ( f -> fa )
rotate( get(f)==get(u) ? f : u);
root = u;
}
void insert(int key){
if (!root) {
root = new Node(key);
return ;
}
int tmp;
Node *cur = root , *last = NULL;
while (1) {
if ( !cur ) {
cur = new Node(key);cur -> fa = last;
last -> ch[ (last->key) < key ] = cur;last->sz++;
splay(cur);
return ;
}
if (cur->key == key) { cur -> cnt++; cur -> sz++; splay(cur); return; }
last = cur;
cur = cur->ch[cur -> key < key];
}
}
void find(int key)
{
Node *cur = root;
while (cur) {
if (cur->key == key ) {
splay(cur);
return;
}
cur = cur -> ch[ cur->key < key ];
}
}
int key_find(int key){
find(key);
return root->ls ? root->ls->sz + 1 : 1;
}
int kth_find(int kth){
Node *cur = root;
while (true) {
if ( cur->ls && cur->ls->sz >= kth ) {
cur = cur->ls;
} else {
kth -= (cur->ls?cur->ls->sz:0) ;
if (kth <= cur->cnt ) {
splay(cur);
return cur->key;
}
kth -= cur->cnt;
cur = cur->rs;
}
}
}
Node* root_pre(){
Node *cur = root->ls;
while (cur->rs) cur = cur->rs;
return cur;
}
Node* root_suf(){
Node *cur = root->rs;
while (cur->ls) cur = cur->ls;
return cur;
}
void Delete(int key){
find(key);
if (root -> cnt > 1) {
root -> cnt--;root -> sz--;
return;
}
if (root -> ls == NULL && root -> rs == NULL ) { root = NULL; delete root; return; }
if (root -> ls == NULL ) { Node* pre = root; root = root->rs; root -> fa = NULL; delete pre; return; }
if (root -> rs == NULL ) { Node* pre = root; root = root->ls; root -> fa = NULL; delete pre; return; }
Node* oldroot = root ; Node *lefbig = root_pre();splay(lefbig);
root -> rs = oldroot -> rs;root -> rs -> fa = root;
delete oldroot;
root -> update();
}
int pre(int key){
Node *cur = root;int ans;
while (cur) {
if (cur -> key < key ) ans = cur->key , cur = cur->rs;
else cur = cur->ls;
}
return ans;
}
int suf(int key){
Node *cur = root;
int ans;
while (cur) {
if (cur -> key > key ) ans = cur->key , cur = cur->ls;
else cur = cur->rs;
}
return ans;
}
那么稍微说一些比较重要的地方:
rotate
中
void rotate(Node *u){
Node *old = u->fa , *oldfa = old->fa;
if ( oldfa )
oldfa -> ch[ oldfa->ch[1]==old ] = u;
u -> fa = oldfa;
int d = old -> ch[1] == u;
old -> ch[d] = u -> ch[d^1];
if (u -> ch[d^1] )u -> ch[d^1] -> fa = old;
u -> ch[d^1] = old;
old -> fa = u;
old->update(); u-> update();
}
那么注意这个地方:
if (u -> ch[d^1] )u -> ch[d^1] -> fa = old;
因为有可能没有这个节点,所以要判一下。
int kth_find(int kth){
Node *cur = root;
while (true) {
if ( cur->ls && cur->ls->sz >= kth ) {
cur = cur->ls;
} else {
kth -= (cur->ls?cur->ls->sz:0) ;
if (kth <= cur->cnt ) {
splay(cur);
return cur->key;
}
kth -= cur->cnt;
cur = cur->rs;
}
}
}
这个是我发现的最好的 kthfind 函数。不解释。
int pre(int key){
Node *cur = root;int ans;
while (cur) {
if (cur -> key < key ) ans = cur->key , cur = cur->rs;
else cur = cur->ls;
}
return ans;
}
找前驱与后继也不用什么先插入再删除这种傻逼做法了。
void splay(Node *u,Node *goal)
{
for (Node *f ; ( f = u -> fa ) != goal ; rotate(u) )
if ( (f -> fa) != goal )
rotate( get(f)==get(u) ? f : u);
if ( goal == NULL ) root = u;
}
这里是把
u
旋转到
这里要注意f的判断。
还有注意,插入或者删除的时候可以不用
update
,直接
sz
,
cnt
变化就可以了。
例题:
- BZOJ 3224 Tyvj 1728:
模板题,不解释。
- BZOJ 1208 [HNOI2004]:
用两棵 splay 模拟这个过程,然后根据加入的点来看如何加入,查查前驱,后继就可以了。但是要注意有可能没有前驱后继的情况,要特判一下。
重点: splay 维护区间信息。
由
splay
的操作可得,我们可以修改树的形状(虽然不能修改连通性),所以我们兴许可以用这个性质来维护一个区间。
我们不妨这样想,如果以下标为关键字,那么我们如果要维护
[l,r]
这个区间时,我们先将
l−1
转到根,然后再将
r+1
转到根的右子树中,这样,根的右子树的左子树就是
[l,r]
这个区间,然后就可以用玄学方法维护了。
比如,维护区间旋转,我们可以打标记,然后下放时将左右子树交换。为什么交换就可以了呢?因为我们输出答案的话是中序输出,那么交换之后我们再中序的时候,根据这个不动点,我们还是可以证明是转成功的。
又比如,像线段树一样维护区间加等。我们仍以下标为关键字,然后每个点记一下子树的总的
cnt
以及他自己的
cnt
,然后就可以像一般的线段树一样下放标记。
那么这里说明一下splay维护区间,或者说序列的典型例题以及需要注意的地方.
- Bzoj1014
题意:给你一个字符串,支持中间插入,单个字词修改。查询
x
位置的后缀与
- 题解:
静态版本就是后缀数组就完了。考虑如何动态。
首先,如果不用后缀数组,那么明显就该用二分加哈希来计算。所以我们
splay
就维护序列的哈希值就完了。
- 一些细节的地方:
一个节点的哈希值是这个节点及其子树表示的序列(一定是连续的一段)的哈希值。
如果没有加入保护节点,那么要时刻注意在头部加入以及在尾部加入的情况。
要寻找第
k
个字符所在的节点的话应当寻找中序遍历的第
还有一个巨大的坑点:
void Insert(int pos,char c){
n++;
if ( pos < n ) ...
else ...
}
这份代码意义很明显。就是在某个位置插入一个字符。然而由于我没有设置保护节点,所以要判一下是不是在最后加入。但是,为什么是先加入n呢?这样子
pos
永远小于
n
,就会
正确写法:
void Insert(int pos,char c){
if ( pos < n ) ...
else ...
n++;
}
- Bzoj 3786
分析,题意什么的就不说了。这里只说一下细节。
如果有
pushdown
函数,而且你又不想像
lct
那样“暴力”,那么
pushdown
写在
splay
里面就好了。
这种维护
dfs
序的东西,你要精确的找的原来的第
k
个位置所对应的点的时候。显然不能
就是说,把新建的节点全部放在数组中。
- Bzoj 3729
注意奇偶要分清。
现在只学了这么多。大概就写到这吧,以后又学到了再填坑。