splay简介
splay是一颗bst,有“序列之王”的美誉,可以处理序列的一切问题。处理区间只有splay和fhq_treap两棵平衡树(当然,如果你想写,B+)可以解决。
优点:万能、常数较小、可用于link cut tree
缺点:代码长、不能可持久化。
相关文章:fhq_treap link cut tree
变量声明
对于一颗合格的splay,我们需要:
int rt, tot, num[N], ch[N][2], fa[N], s[N], v[N], tag[N];
这个程序基于序列操作,要求的是区间翻转。
rt:树根
tot:目前可用的是哪个位置(和主席树回收空间的方法差不对)
num:初始序列
ch:儿子编号
fa:父亲编号
s:该节点的子树大小
v:splay数组
tag:懒标记
来一波我的鬼畜define
#define get(x) (ch[fa[x]][1]==x)
#define ls ch[x][0]
#define rs ch[x][1]
get(x)的意思是x是否为其父的右子树。
splay操作
void splay(x,goal)
我们假设有一棵原树,长成这样:
splay树满足如下的性质:中序遍历是有序的(当处理普通平衡树问题的时候,且合法的操作都符合特性)。
注意:上述提醒对于序列操作无效,只是当你推导有困难的时候自己画图(脑补)考虑一下。
通过将节点旋转至根节点来进一步操作(称为splay操作)
那么我们先来看splay操作:
第一种情况、需旋转的节点x的父亲是目标节点p。那么只要一次旋转。如图x是左儿子,需要右旋。图为将a旋转到b。图片来自百度
第二种情况、x、x的父亲、x的父亲的父亲“三点共线”(如果不能理解就看下面的图),执行两次相同方向的旋转。那么先旋转x的父节点,再旋转x。图为将x点旋转到z点。图片来自百度。
第三种情况、else。执行两次不同方向的旋转。图为将x旋转到g。
上述三种情况介绍完之后,相信同学们对于旋转(还不懂?往下翻)以及splay操作有一定的认识。
递归写法:(来源:算法竞赛入门经典)
void splay(Node* &o,int k) {
int d=o->cmp(k);
if (d==1) k-=o->ch[0]->s+1;
if (d!=-1) {
Node *p=o->ch[d];
int d2=p->cmp(k);
int k2=(d2==0>k:k-p->ch[0]->s-1);
if (d2!=-1) {
splay(p->ch[d2],k2);
if (d==d2) rotate(o,d^1); else rotate(o->ch[d],d);
}
}
rotate(o,d^1);
}
非递归写法(建议同学们背一下):
void splay(int x,int goal=0) {
for (int f;(f=fa[x])!=goal;rotate(x))
if (fa[f]!=goal) rotate((get(x)==get(f))?f:x);
if (!goal) rt=x;
}
void rotate(x)
贴一张一次旋转的图。
就是上述过程中的旋转。需要完成下面4件事情:
X变到原来Y的位置
Y变成了 X原来在Y的 相对的那个儿子
Y的非X的儿子不变 X的 X原来在Y的 那个儿子不变
X的 X原来在Y的 相对的 那个儿子 变成了 Y原来是X的那个儿子
上代码。
bool get(int x) {return ch[fa[x]][1]==x;}
void rotate(int x) {
int y=fa[x],z=fa[y],k=get(x);
//y是x的父亲,z是父亲的父亲,k是x是否是它父亲的右子树。
pushdown(y); pushdown(x); //不要忘记pushdown
ch[y][k]=ch[x][k^1]; fa[ch[y][k]]=y;
ch[x][k^1]=y; fa[y]=x;
fa[x]=z;
if (z) ch[z][ch[z][1]==y]=x;
pushup(y); pushup(x); //pushup
}
int query_kth(x)
这个操作是名次树中的求名次。主要就是沿着树找下去,找到位置返回即可。
**如果x比当前结点小,即应该向左子树寻找,ans不用改变。**一直往左子树走,中序遍历最小
如果x比当前结点大,即应该向右子树寻找,ans需要加上左子树的大小以及根的大小。 易证
如果同学们仍然不能理解,我贴一张图:
好。明白了吧。
inline int kth(int x) {
int pos=rt;
while (pos) {
pushdown(pos);
if (x<=s[ch[pos][0]]) pos=ch[pos][0];
else {
x-=s[ch[pos][0]]+1;
if (!x) return pos; //此时x和树中的点重合,树中不允许有两个相同的点
pos=ch[pos][1]; //到达右孩子处
}
}
}
void build(f, l, r)
因为splay在splay的过程中会变得平衡,所以build可以随便建。可以参考线段树的build。时间复杂度 O ( N ) O(N) O(N)
inline int build(int l,int r,int f) {
if (l>r) return 0;
int x=++tot, mid=(l+r)/2;
fa[x]=f, v[x]=mx[x]=num[mid];
ch[x][0]=build(l,mid-1,x);
ch[x][1]=build(mid+1,r,x);
pushup(x); return x;
}
void insert(x)
和普通的平衡树insert一样。简单说就是找到位置开点。想不懂的看一看querykth的那张图。
inline void insert(int x) {
int u=root, ff=0;
while (u && t[u].val!=x) {
ff=u;
u=t[u].ch[x>t[u].val];
}
if (u) t[u].cnt++;
else {
u=++tot;
if (ff) t[ff].ch[x>t[ff].val]=u;
t[u].ch[0]=t[u].ch[1]=0;
t[tot].ff=ff;
t[tot].val=x;
t[tot].cnt=1;
t[tot].size=1;
}
splay(u, 0);
}
区间操作
这里的区间操作是把l转到根节点,r转到l节点,那么所求的区间就在根的右子树的左子树。
(真想配个图,但是我画不出来……有同学画出来图联系我吧)
void turn(int l,int r)
{
l=query_kth(l); r=query_kth(r+2);
//先让l占下根的位置,然后让r+2把他挤到左子树上去
splay(l,0); splay(r,l);
pushdown(rt);
tag[ch[ch[rt][1]][0]]^=1;//根的右子树的左子树
}
pushup、pushdown
只要记住要在修改、访问之前加pushdown,在修改之后加pushup。
下面是区间翻转操作的代码。
void pushup(int x) { siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1; }
void pushdown(int x)
{
if (x && tag[x]) {
tag[ch[x][0]]^=1;
tag[ch[x][1]]^=1;
swap(ch[x][0],ch[x][1]);
tag[x]=0;
}
}
split merge
不知道同学们有没有学过fhq_treap。我们都知道,fhq_treap是通过split和merge两个操作打天下的。
split指的是将某个节点的子树与其父断开,而merge指的是把断开后的连上去。
如图。
上代码:
//合并操作:把序列s1, s2连接在一起,s1在左边
Node* merge(Node* left, Node* right) {
splay(left, left->s);
left->ch[1] = right;
left->maintain();
return left;
}
//分裂操作:把序列S分成两个连续的子序列,左子序列包含k个元素,右子序列包含剩下的元素
void split(Node* o, int k, Node* &left, Node* &right) {
splay(o, k);
left = o;
right = o->ch[1];
o->ch[1] = NULL;
left->maintain();
}
模板题
模板题应该不需要我加解析吧……
例1:luogu3391文艺平衡树
这代码不是我写的……感谢热心学长……
#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
int n,m,i,j,t,k,s,rt,tot,a[100005],ch[100005][2],fa[100005],key[100005],cnt[100005],siz[100005],tag[100005];
void pushup(int x) { siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1; }
void pushdown(int x) {
if (x && tag[x]) {
tag[ch[x][0]]^=1;
tag[ch[x][1]]^=1;
swap(ch[x][0],ch[x][1]);
tag[x]=0;
}
}
bool get(int x) {return ch[fa[x]][1]==x;}
void rotate(int x) {
int y=fa[x],z=fa[y],k=get(x);
//y是x的父亲,z是父亲的父亲,k是x是否是它父亲的右子树。
pushdown(y); pushdown(x); //不要忘记pushdown
ch[y][k]=ch[x][k^1]; fa[ch[y][k]]=y;
ch[x][k^1]=y; fa[y]=x;
fa[x]=z;
if (z) ch[z][ch[z][1]==y]=x;
pushup(y); pushup(x); //pushup
}
void splay(int x,int goal=0) {
for (int f; (f=fa[x])!=goal; rotate(x))
if (fa[f]!=goal)
rotate((get(x)==get(f))?f:x);
if (!goal) rt=x;
}
int query_kth(int x) {
int now=rt;
while (1) {
pushdown(now);
if (x<=siz[ch[now][0]]) now=ch[now][0];
else {
x-=siz[ch[now][0]]+1;
if (!x) return now;
now=ch[now][1];
}
}
}
int build(int f,int l,int r) {
if (l>r) return 0;
int mid=(l+r)>>1;
int now=++tot;
key[now]=a[mid]; fa[now]=f; tag[now]=0;
ch[now][0]=build(now,l,mid-1);
ch[now][1]=build(now,mid+1,r);
pushup(now);
return now;
}
void turn(int l,int r) {
l=query_kth(l); r=query_kth(r+2);
//先让l占下根的位置,然后让r+2把他挤到左子树上去
splay(l,0); splay(r,l);
pushdown(rt);
tag[ch[ch[rt][1]][0]]^=1;//根的右子树的左子树
}
void print(int now) {
pushdown(now);
if (ch[now][0]) print(ch[now][0]);
if (key[now]!=-INF && key[now]!=INF) {
if (t) printf(" ");
printf("%d",key[now]);
t=1;
}
if (ch[now][1]) print(ch[now][1]);
}
int main() {
scanf("%d%d",&n,&m);
for (int i=1; i<=n; i++) a[i+1]=i;
a[1]=-INF;a[n+2]=INF;
rt=build(0,1,n+2);
for (int i=1; i<=m; i++) {
int x,y;
scanf("%d%d",&x,&y);
turn(x,y);
}
print(rt);
printf("\n");
return 0;
}
例2:luogu4146 序列终结者
// luogu-judger-enable-o2
#include<bits/stdc++.h>
using std::max;
using std::swap;
#define gc getchar()
#define get(x) (ch[fa[x]][1]==x)
#define ls ch[x][0]
#define rs ch[x][1]
inline void read(int& t) {
t=0; int f=1; char ch=gc;
while (ch<'0' || ch>'9') {if (ch=='-') f=-1; ch=gc;}
do {t=t*10+ch-'0'; ch=gc;} while (ch>='0' && ch<='9');
t*=f;
}
const int N=100005;
int n,m,rt,tot,num[N],ch[N][2],fa[N],s[N],v[N],add[N],rev[N],mx[N];
inline void pushup(int x) {
s[x]=s[ch[x][1]]+s[ch[x][0]]+1;
mx[x]=max(v[x],max(mx[ch[x][1]],mx[ch[x][0]]));
}
inline void pushdown(int x) {
if (add[x]) {
if (ls) add[ls]+=add[x], v[ls]+=add[x], mx[ls]+=add[x];
if (rs) add[rs]+=add[x], v[rs]+=add[x], mx[rs]+=add[x];
}
if (rev[x]) rev[ls]^=1, rev[rs]^=1, swap(ls,rs);
rev[x]=add[x]=0;
}
inline void rotate(int x) {
int y=fa[x], z=fa[fa[x]], k=get(x);
pushdown(y), pushdown(x);
ch[y][k]=ch[x][k^1], fa[ch[y][k]]=y;
ch[x][k^1]=y, fa[y]=x, fa[x]=z;
if (z) ch[z][ch[z][1]==y]=x;
pushup(y), pushup(x);
}
inline void splay(int x, int g) {
for (int j; (j=fa[x])!=g; rotate(x))
if (fa[j]!=g) rotate((get(x)==get(j))?j:x);
if (!g) rt=x;
}
inline int build(int l,int r,int f) {
if (l>r) return 0;
int x=++tot, mid=(l+r)/2;
fa[x]=f, v[x]=mx[x]=num[mid];
ch[x][0]=build(l,mid-1,x);
ch[x][1]=build(mid+1,r,x);
pushup(x); return x;
}
inline int kth(int x) {
int pos=rt;
while (pos) {
pushdown(pos);
if (x<=s[ch[pos][0]]) pos=ch[pos][0];
else {
x-=s[ch[pos][0]]+1;
if (!x) return pos;
pos=ch[pos][1];
}
}
}
int main() {
read(n), read(m);
mx[0]=num[1]=num[n+2]=-2e9, rt=build(1, n+2, 0);
while (m--) {
int k, l, r, vv, s;
read(k), read(l), read(r);
int L=kth(l), R=kth(r+2);
splay(L,0), splay(R,L);
s=ch[ch[rt][1]][0];
if (k==1) read(vv), add[s]+=vv, mx[s]+=vv, v[s]+=vv;
else if (k==2) rev[s]^=1;
else printf("%d\n",mx[s]);
}
return 0;
}