Splay(伸展树)学习笔记
Splay 时间复杂度证明
咕咕。
Splay 基本操作
具体原理
可以参考以下博客(个人觉得写得比较好):
普通平衡树(权值Splay)
前置变量声明
const int N = 1e5+7;
int rt,tot; //Splay的根,Splay总结点数
int val[N]; //val[u] u的权值
int fa[N],ch[N][2]; //fa[u] u的父亲,ch[u][0]左儿子,ch[u][1]右儿子
int cnt[N]; //cnt[u] 有几个权值都是 val[u] 的数
int sz[N]; //sz[u] u的子树大小
前置操作
inline int get(int x) { return ch[fa[x]][1] == x; }
inline void push_up(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + cnt[x]; }
get(x):\(x\) 是父亲的左儿子还是右儿子,左儿子返回 \(0\),右儿子返回 \(1\)。
push_up(x):更新 \(x\) 的子树大小。
旋转 rotate
最重要的操作,Splay 的核心。
inline void rotate(int x) {
int y = fa[x], z = fa[y], chk = get(x), t = ch[x][chk^1];
ch[z][get(y)] = x, fa[x] = z; ch[x][chk^1] = y, fa[y] = x; ch[y][chk] = t, fa[t] = y;
push_up(y); push_up(x);
}
(注意代码赋值顺序)
旋转虽然分为左旋和右旋,但自己手推后可以发现左旋和右旋是相互对称的,因此可以合并在一起写。下面给出右旋一张具体的图:

伸展 splay
void splay(int x,int goal) { //将 x 旋转至 goal 的儿子
while(fa[x] != goal) {
int y = fa[x], z = fa[y];
if(z != goal) rotate((get(x)==get(y) ? y : x));
rotate(x);
}
if(goal == 0) rt = x;
}
每次连续旋转两次,如果三点一线就先旋父亲,这个和 Splay 的复杂度分析有关,原理先咕咕。
插入 insert
void insert(int x) { //插入一个权值为 x 的数
int now = rt, fath = 0;
while(now!=0 && val[now]!=x) {
fath = now; now = ch[now][x>val[now]];
}
if(now != 0) {
++cnt[now];
} else {
now = ++tot; fa[now] = fath, ch[fath][x>val[fath]] = now; val[now] = x; cnt[now] = 1; sz[now] = 1;
}
splay(now,0);
}
插入后将新节点旋转到根,这个和 Splay 的复杂度分析有关,原理先咕咕。
查找 find
void find(int x) { //找到 x 这个值对应的结点并将其旋转至根
int now = rt;
if(!now) return ;
while(ch[now][x>val[now]]!=0 && val[now]!=x) {
now = ch[now][x>val[now]];
}
splay(now, 0);
}
查找操作可以为前驱和后继操作服务。
注意: find 操作结束后,根节点(也就是 \(x\) 这个值对应的结点)的左子树大小 \(+1\),即 \(sz[ch[rt,0]]+1\),就代表 \(x\) 这个值在集合中的排名。
前驱 pre
int pre(int x) { //找到数 x 的前驱,并返回这个结点的编号
find(x);
if(val[rt] < x) return val[rt];
//当集合中没有权值为 x 的这个结点,如果查找到的结点就小于 x,那么一定比自己的左儿子更接近 x
int now = ch[rt][0];
while(ch[now][1] > 0) {
now = ch[now][1];
}
return val[now];
}
后继 nxt
int nxt(int x) { //找到数 x 的后继,并返回这个结点的编号
find(x);
if(val[rt] > x) return val[rt];
//当集合中没有权值为 x 的这个结点,如果查找到的结点就大于 x,那么一定比自己的右儿子更接近 x
int now = ch[rt][1];
while(ch[now][0] > 0) {
now = ch[now][0];
}
return val[now];
}
删除 del
void del(int x) { //删除权值为 x 的结点
int p = pre(x), q = nxt(x);
splay(p,0), splay(q,p);
if(cnt[ch[q][0]] > 1) {
--cnt[ch[q][0]]; splay(ch[q][0], 0);
} else {
ch[q][0] = 0;
}
}
将操作后的结点(如果 \(cnt[u]>1\),没有将结点删除)旋转到根,可能和 Splay 的复杂度分析有关,原理先咕咕。
查询 \(k\) 小值 kth
int kth(int k) { //找到排名为 k 的数并返回
if(sz[rt] < k) return -1; //没有排名为 k 的数
int now = rt;
while(true) {
if(k <= sz[ch[now][0]]) now = ch[now][0];
else if(k <= sz[ch[now][0]]+cnt[now]) return val[now];
else k -= sz[ch[now][0]]+cnt[now], now = ch[now][1];
}
}
完整版 Code
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); }
return x * f;
}
const int N = 1e5+7;
int m;
struct Splay {
int rt,tot; //Splay的根,Splay总结点数
int val[N]; //val[u] u的权值
int fa[N],ch[N][2]; //fa[u] u的父亲,ch[u][0]左儿子,ch[u][1]右儿子
int cnt[N]; //cnt[u] 有几个权值都是 val[u] 的数
int sz[N]; //sz[u] u的子树大小
inline int get(int x) { return ch[fa[x]][1] == x; }
inline void push_up(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + cnt[x]; }
inline void rotate(int x) {
int y = fa[x], z = fa[y], chk = get(x), t = ch[x][chk^1];
ch[z][get(y)] = x, fa[x] = z; ch[x][chk^1] = y, fa[y] = x; ch[y][chk] = t, fa[t] = y;
push_up(y); push_up(x);
}
void splay(int x,int goal) { //将 x 旋转至 goal 的儿子
while(fa[x] != goal) {
int y = fa[x], z = fa[y];
if(z != goal) rotate((get(x)==get(y) ? y : x));
rotate(x);
}
if(goal == 0) rt = x;
}
void insert(int x) { //插入一个权值为 x 的数
int now = rt, fath = 0;
while(now!=0 && val[now]!=x) {
fath = now; now = ch[now][x>val[now]];
}
if(now != 0) {
++cnt[now];
} else {
now = ++tot; fa[now] = fath, ch[fath][x>val[fath]] = now; val[now] = x; cnt[now] = 1; sz[now] = 1;
}
splay(now,0);
}
void find(int x) { //找到 x 这个值对应的结点并将其旋转至根
int now = rt;
if(!now) return ;
while(ch[now][x>val[now]]!=0 && val[now]!=x) {
now = ch[now][x>val[now]];
}
splay(now, 0);
}
int pre(int x) { //找到数 x 的前驱,并返回这个结点的编号
find(x);
if(val[rt] < x) return rt;
//当集合中没有权值为 x 的这个结点,如果查找到的结点就小于 x,那么一定比自己的左儿子更接近 x
int now = ch[rt][0];
while(ch[now][1] > 0) {
now = ch[now][1];
}
return now;
}
int nxt(int x) { //找到数 x 的后继,并返回这个结点的编号
find(x);
if(val[rt] > x) return rt;
//当集合中没有权值为 x 的这个结点,如果查找到的结点就大于 x,那么一定比自己的右儿子更接近 x
int now = ch[rt][1];
while(ch[now][0] > 0) {
now = ch[now][0];
}
return now;
}
void del(int x) { //删除权值为 x 的结点
int p = pre(x), q = nxt(x);
splay(p,0), splay(q,p);
if(cnt[ch[q][0]] > 1) {
--cnt[ch[q][0]]; splay(ch[q][0], 0);
} else {
ch[q][0] = 0;
}
}
int kth(int k) { //找到排名为 k 的数并返回
if(sz[rt] < k) return -1; //没有排名为 k 的数
int now = rt;
while(true) {
if(k <= sz[ch[now][0]]) now = ch[now][0];
else if(k <= sz[ch[now][0]]+cnt[now]) return val[now];
else k -= sz[ch[now][0]]+cnt[now], now = ch[now][1];
}
}
int rk(int x) {
find(x); return sz[ch[rt][0]] + 1;
}
}T;
int main()
{
m = read();
T.insert(-INF), T.insert(INF);
while(m--) {
int opt = read(), x = read();
if(opt == 1) {
T.insert(x);
} else if(opt == 2) {
T.del(x);
} else if(opt == 3) {
printf("%d\n",T.rk(x)-1);
} else if(opt == 4) {
printf("%d\n",T.kth(x+1));
} else if(opt == 5) {
printf("%d\n",T.val[T.pre(x)]);
} else if(opt == 6) {
printf("%d\n",T.val[T.nxt(x)]);
}
}
return 0;
}
/*
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598
106465
84185
492737
*/
文艺平衡树(序列 Splay)
在维护序列操作的 Splay 上,排名为 \(i\) 的结点对应在序列上下标为 \(i\) 的数。
性质1: 中序遍历的树,只要不断交换左右子树就可以实现序列翻转。
手推一下即可。
因此我们可以用 Splay + 懒标记实现。
Code
#include<bits/stdc++.h>
using namespace std;
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); }
return x * f;
}
const int N = 1e5+7;
int n,m;
struct Splay {
int rt,tot;
int fa[N],ch[N][2],lazy[N],sz[N];
inline int get(int x) {
return ch[fa[x]][1] == x;
}
inline void push_up(int x) {
sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + 1;
}
inline void push_down(int x) {
if(lazy[x] == 1) {
lazy[ch[x][0]] ^= 1, lazy[ch[x][1]] ^= 1; swap(ch[x][0],ch[x][1]); lazy[x] = 0;
}
}
inline void rotate(int x) {
push_down(x);
int y = fa[x], z = fa[y], chk = get(x), t = ch[x][chk^1];
ch[z][get(y)] = x, fa[x] = z; ch[x][chk^1] = y, fa[y] = x; ch[y][chk] = t, fa[t] = y;
push_up(y); push_up(x);
}
void splay(int x,int goal) { //将 x 旋转到 goal 的儿子
while(fa[x] != goal) {
int y = fa[x], z = fa[y];
if(z != goal) rotate((get(x)==get(y) ? y : x));
rotate(x);
}
if(goal == 0) rt = x;
}
void insert(int x) {
int now = rt, fath = 0;
while(now != 0) {
fath = now; now = ch[now][x>now];
}
now = ++tot; ch[fath][x>fath] = now; fa[now] = fath; sz[now] = 1; lazy[now] = 0;
splay(now,0);
}
int find(int x) { //找的序列中下标为 x 的在splay树上代表的结点
int now = rt;
if(x > sz[rt]) return -1;
while(true) {
push_down(now);
if(x <= sz[ch[now][0]]) now = ch[now][0];
else if(x == sz[ch[now][0]]+1) return now;
else x -= sz[ch[now][0]]+1, now = ch[now][1];
}
}
void Turn(int l,int r) {
int x = find(l), y = find(r+2);
splay(x,0), splay(y,x);
lazy[ch[y][0]] ^= 1;
}
void Print(int now) {
push_down(now);
if(ch[now][0]) Print(ch[now][0]);
if(1<now && now<n+2) printf("%d ",now-1);
if(ch[now][1]) Print(ch[now][1]);
}
}T;
int main()
{
n = read(), m = read();
for(int i=1;i<=n+2;++i) T.insert(i);
while(m--) {
int l = read(), r = read();
T.Turn(l,r);
}
T.Print(T.rt);
return 0;
}
/*
5 3
1 3
1 3
1 4
4 3 2 1 5
*/
更多练习题
咕咕。

浙公网安备 33010602011771号