平衡树(splay)
平衡树能干些什么呢?
插入一个数
删除一个数
查询数 x x 的排名(小于的元素个数)
查询排名为 x x 的数
查询的前驱(小于 x x 且最大的数)
查询的后继(大于 x x 且最小的数)
区间翻转
区间加乘(像线段树一样打标记即可)
但是平衡树有一个缺陷,不能在区间层面上进行上述操作
所以我们可以在外面套上一层线段树(线段树套)
线段树的每一个结点都是一棵
splay
s
p
l
a
y
这样我们在构建的时候,只能单点插入,而且一条路上的每一个结点都要insert,同理,修改也只支持单点
线段树套
splay
s
p
l
a
y
,基本上只具有线段树询问区间的功能
并且支持
splay
s
p
l
a
y
的几乎所有功能
插入一个数
删除一个数
查询数 x x 的排名(小于的元素个数)
查询排名为 x x 的数(二分判定)
查询的前驱(小于 x x 且最大的数)
(分成个区间,每个区间求前驱,最后求 max m a x )查询 x x 的后继(大于且最小的数)
(分成 logn l o g n 个区间,每个区间求后继,最后求 min m i n )区间翻转(没写过)
区间加乘(像线段树一样打标记即可)
所以说如果发现一道题需要支持上述操作,我们就可以考虑
splay
s
p
l
a
y
(不过如果只有查询元素排名以及排名为k的元素,还是先考虑一下主席树,毕竟还是代码复杂度越低越好)
经典例题:splay区间翻转
经典例题:线段树+splay(2B)
经典例题:线段树+splay(逆序对个数)
经典例题:splay(括号)
经典例题:splay(项链工厂)
splay
s
p
l
a
y
最基础的操作
注意
splay
s
p
l
a
y
函数和
del
d
e
l
函数的写法
insert
i
n
s
e
r
t
的时候不要忘了
update(last)
u
p
d
a
t
e
(
l
a
s
t
)
如果要加上
rever
r
e
v
e
r
我们先调用
find_pm
f
i
n
d
_
p
m
夹逼我们需要翻转的区间
之后再这个区间打上翻转标记即可
不过,这样我们就需要在
splay
s
p
l
a
y
之前
down
d
o
w
n
一下
每次
find
f
i
n
d
的时候也需要
push
p
u
s
h
(多
push
p
u
s
h
没有什么坏处)
#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
const int N=100010;
int root=0,top=0,pre[N],ch[N][2],size[N],v[N],cnt[N];
bool rev[N];
void clear(int bh) {
pre[bh]=ch[bh][0]=ch[bh][1]=size[bh]=v[bh]=cnt[bh]=0;
//rev[bh]=0;
}
int get(int bh) {
return ch[pre[bh]][0]==bh? 0:1;
}
void push(int bh) {
if (!bh||!rev[bh]) return;
if (ch[bh][0]) rev[ch[bh][0]]^=1;
if (ch[bh][1]) rev[ch[bh][1]]^=1;
swap(ch[bh][0],ch[bh][1]);
}
void down(int bh) {
if (pre[bh]) down(pre[bh]);
push(bh);
}
void update(int bh) {
if (!bh) return;
size[bh]=cnt[bh];
if (ch[bh][0]) size[bh]+=size[ch[bh][0]];
if (ch[bh][1]) size[bh]+=size[ch[bh][1]];
}
void rotate(int bh) {
int fa=pre[bh];
int grand=pre[fa];
int wh=get(bh);
ch[fa][wh]=ch[bh][wh^1];
pre[ch[fa][wh]]=fa;
ch[bh][wh^1]=fa;
pre[fa]=bh;
pre[bh]=grand;
if (grand) ch[grand][ch[grand][0]==fa? 0:1]=bh;
update(fa);
update(bh);
}
void splay(int bh,int mb) {
//down(bh);
for (int fa;(fa=pre[bh])!=mb;rotate(bh))
if (pre[fa]!=mb) //pre[fa]!=mb
rotate(get(bh)==get(fa)? fa:bh);
if (!mb) root=bh;
}
void insert(int x) {
int now=root;
int last=0;
if (!root) {
top++;
pre[top]=0; ch[top][0]=ch[top][1]=0;
size[top]=1; cnt[top]=1; v[top]=x;
root=top; //root=top;
return;
}
while (1) {
//push(now);
if (v[now]==x) {
cnt[now]++;
update(now);
update(last); //update(last)
splay(now,0);
return;
}
last=now;
now=ch[now][v[now]<x];
if (!now) {
top++;
pre[top]=last; ch[top][0]=ch[top][1]=0;
size[top]=1; cnt[top]=1; v[top]=x;
ch[last][v[last]<x]=top;
update(last);
splay(top,0);
return;
}
}
}
int find_pm(int x) {
int now=root,ans=0;
while (now) {
//push(now);
if (v[now]>x) now=ch[now][0];
else {
ans+=(ch[now][0]? size[ch[now][0]]:0); //先维护ans
if (v[now]==x) {
splay(now,0);
return ans+1;
}
ans+=cnt[now];
now=ch[now][1];
}
}
return 0;
}
int find_x(int k) {
int now=root;
while (now) {
//push(now);
if (ch[now][0]&&size[ch[now][0]]>=k) now=ch[now][0];
else {
int tmp=(ch[now][0])? size[ch[now][0]]:0;
tmp+=cnt[now];
if (tmp>=k) return now;
k-=tmp;
now=ch[now][1];
}
}
return 0;
}
int qian() {
int x=ch[root][0];
while (ch[x][1]) x=ch[x][1];
return x;
}
void del(int x) {
find_pm(x);
if (cnt[root]>1) {
cnt[root]--;
update(root);
return;
}
if (!ch[root][0]&&!ch[root][1]) {
clear(root);
root=0;
return;
}
if (!ch[root][0]) {
int k=root;
root=ch[k][1];
pre[root]=0;
clear(k);
return;
}
if (!ch[root][1]) {
int k=root;
root=ch[k][0];
pre[root]=0;
clear(k);
return;
}
int k=root;
int p=qian();
splay(p,0);
ch[root][1]=ch[k][1];
pre[ch[root][1]]=root;
update(root);
clear(k);
}
int fro(int x) {
int ans=0;
int now=root;
while (now) {
//push(now);
if (v[now]>=x) now=ch[now][0];
else {
ans=max(ans,v[now]);
now=ch[now][1];
}
}
return ans;
}
int nxt(int x) {
int ans=INF;
int now=root;
while (now) {
//push(now);
if (v[now]<=x) now=ch[now][1];
else {
ans=min(ans,v[now]);
now=ch[now][0];
}
}
return ans;
}
int main()
{
int x,opt;
int n;
scanf("%d",&n);
while (n--) {
scanf("%d%d",&opt,&x);
if (opt==1) insert(x);
else if (opt==2) del(x);
else if (opt==3)
printf("%d\n",find_pm(x));
else if (opt==4)
printf("%d\n",v[find_x(x)]);
else if (opt==5)
printf("%d\n",fro(x)); //不保证数据中存在x
else
printf("%d\n",nxt(x)); //不保证数据中存在x
}
return 0;
}
insert
i
n
s
e
r
t
函数支持单点插入
如果我们有初始序列,那么推荐如此建树:
int build(int l,int r,int fa)
{
if (l>r) return 0;
int mid=(l+r)>>1;
int now=++top;
ch[now][0]=build(l,mid-1,now);
ch[now][1]=build(mid+1,r,now);
pre[now]=fa;
rev[now]=0;
v[now]=a[mid];
update(now);
return now;
}
LCT
实际上
LCT
L
C
T
就是动态的多个
splay
s
p
l
a
y
,重点还是理解操作
(注意,LCT一定是一棵TREE)
LCT L C T 中最重要的两个操作(除了 splay s p l a y 和 rotate r o t a t e )就是 expose e x p o s e 和 makeroot m a k e r o o t
expose e x p o s e
访问一个结点,该结点到根的路径就会变成偏爱路径,在一棵 splay s p l a y 上
void expose(int x) {
int t=0;
while (x) {
splay(x);
ch[x][1]=t;
update(x); //产生了新儿子,需要update
t=x;
x=pre[x];
}
}
makeroot m a k e r o o t
换根,把树形结构的根换为 x x
void makeroot(int x) {
expose(x);
splay(x);
rev[x]^=1;
}
其余的所有操作都是建立在这两个操作上的
我们要将
x
x
和连接起来
简单的,我们把
x
x
连到上
明确
x,y
x
,
y
一定在不同的
splay
s
p
l
a
y
内(废话,ta们都不连通)
那么我们首先需要
x
x
没有,这样我们才能放心大胆的把ta连向
y
y
什么样的结点没有?根结点!
所以我们
makeroot(x)
m
a
k
e
r
o
o
t
(
x
)
因为我们没有访问过
x<−>y
x
<
−
>
y
这条路径,所以
x
x
不会是的偏爱儿子
换句话说,
y
y
是不会认这个儿子的
所以我们只要
pre[x]=y
p
r
e
[
x
]
=
y
就可以了
void link(int x,int y) {
makeroot(x);
pre[x]=y;
}
cut(x,y) c u t ( x , y )
和link一样,我们需要把
x
x
变成树的根:makeroot(x)
之后
我们就得到了一棵以
y
y
为根,路径的
splay
s
p
l
a
y
x
x
是根节点,ta的深度最小,是辅助树的根节点
所以
x
x
一定是的左儿子
pre[x]=0,ch[y][0]=0
p
r
e
[
x
]
=
0
,
c
h
[
y
]
[
0
]
=
0
void cut(int x,int y) {
makeroot(x);
expose(y);
splay(y);
ch[y][0]=pre[x]=0;
//update(y)
}
find f i n d
这个操作可以找到
x
x
结点在原树中的根结点
(根结点的深度最小,所以在中最靠左)
int find(int x) {
expose(x);
splay(x);
while (ch[x][0]) x=ch[x][0];
return x;
}
两点连通性
bool linked(int x,int y) {
return find(x)==find(y);
}
路径权值和
LCT的优点就是ta的形态不固定
我们可以把路径中的一个端点视为根节点(比如说x):
makeroot(x)
m
a
k
e
r
o
o
t
(
x
)
expose(y)
e
x
p
o
s
e
(
y
)
,
x
x
到的路径就变成了偏爱路径,
splay(y)
s
p
l
a
y
(
y
)
,
y
y
节点上的即为路径权值和
int ask_sum(int x,int y) {
makeroot(x);
expose(y); splay(y);
return sum[y];
}
节点到根的距离:
同理,返回 size s i z e 即可
int ask_dis(int x,int y) {
makeroot(x);
expose(y); splay(y);
return size[y];
}
更改节点值
我们要改变一个结点的权值, 我们当然希望涉及到的结点尽量少
什么样的结点改变ta的值影响的结点维护值最少?根节点!
所以我们首先
makeroot(x)
m
a
k
e
r
o
o
t
(
x
)
直接修改
x
x
的值就可以了
因为我们改变了这个结点的状态,所以需要
void change_point(int x,int z) {
makeroot(x);
v[x]=z;
update(x);
}
更改路径值
这个操作比较厉害,可以处理路径加乘的问题
我们像线段树的加乘那样每个结点维护加标记和乘标记
每次在push的时候,维护值以及标记
mul[son]=mul[son]∗mul[fa],ad[son]=ad[son]∗mul[fa]+ad[fa]
m
u
l
[
s
o
n
]
=
m
u
l
[
s
o
n
]
∗
m
u
l
[
f
a
]
,
a
d
[
s
o
n
]
=
a
d
[
s
o
n
]
∗
m
u
l
[
f
a
]
+
a
d
[
f
a
]
void cal(int x,int a,int b) { //*a +b
if (!x) return;
v[x]=v[x]*a+b; //不要忘了维护单点值
sum[x]=sum[x]*a+size[x]*b;
ad[x]=ad[x]*a+b;
mul[x]=mul[x]*a;
}
void push(int bh) {
if (!bh) return;
if (rev[bh]) {
...
}
if (ad[bh]!=0||mul[bh]!=1) {
if (ch[bh][0]) cal(ch[bh][0],mul[bh],ad[bh]);
if (ch[bh][1]) cal(ch[bh][1],mul[bh],ad[bh]);
}
mul[bh]=1; ad[bh]=0;
}
void add(int x,int y,int z) {
makeroot(x);
expose(y); splay(y);
cal(y,1,z);
}
void multi(int x,int y,int z) {
makeroot(x);
expose(y); splay(y);
cal(y,z,0);
}
终上所述,需要询问树上路径权值,同时需要支持加边删边操作的题目,可以考虑LCT
经典例题:LCT基本操作
经典例题:LCT维护路径加乘
经典例题:LCT+并查集(一)
经典例题:LCT+并查集(二)
经典例题:LCT+SAM
LCT中也要判断:if (!bh) return;
const int N=100010;
int pre[N],ch[N][0],size[N],sum[N],v[N],ad[N],mul[N];
bool rev[N];
int q[N];
int isroot(int bh) {
return ch[pre[bh]][0]!=bh&&ch[pre[bh]][1]!=bh;
}
int get(int bh) {
return ch[pre[bh]][0]==bh? 0:1;
}
void update(int bh) {
if (!bh) return;
size[bh]=1;
if (ch[bh][0]) size[bh]+=size[ch[bh][0]];
if (ch[bh][1]) size[bh]+=size[ch[bh][1]];
sum[bh]=v[bh];
if (ch[bh][0]) sum[bh]+=sum[ch[bh][0]];
if (ch[bh][1]) sum[bh]+=sum[ch[bh][1]];
}
void rotate(int bh) {
int fa=pre[bh];
int grand=pre[fa];
int wh=get(bh);
if (!isroot(fa)) ch[grand][ch[grand][0]==fa? 0:1]=bh;
pre[bh]=grand;
ch[fa][wh]=ch[bh][wh^1];
pre[ch[fa][wh]]=fa;
ch[bh][wh^1]=fa;
pre[fa]=bh;
update(fa);
update(bh);
}
void push(int bh) {
if (!rev[bh]||!bh) return;
if (ch[bh][0]) rev[ch[bh][0]]^=1;
if (ch[bh][1]) rev[ch[bh][1]]^=1;
swap(ch[bh][0],ch[bh][1]);
rev[bh]=0;
}
void splay(int bh) {
int top=0;
q[++top]=bh;
for (int i=bh;!isroot(i);i=pre[i]) q[++top]=pre[i];
while (top) push(q[top--]);
for (int fa;!(isroot(bh));rotate(bh))
if (!isroot(fa=pre[bh]))
rotate(get(fa)==get(bh)? fa:bh);
}
void expose(int x) {
int t=0;
while (x) {
splay(x);
ch[x][1]=t;
update(x); //产生了新儿子,需要update
t=x;
x=pre[x];
}
}
void makeroot(int x) {
expose(x);
splay(x);
rev[x]^=1;
}
void link(int x,int y) {
makeroot(x);
pre[x]=y;
}
void cut(int x,int y) {
makeroot(x);
expose(y);
splay(y);
ch[y][0]=pre[x]=0;
//update(y)
}
int find(int x) {
expose(x);
splay(x);
while (ch[x][0]) x=ch[x][0];
return x;
}
bool linked(int x,int y) {
return find(x)==find(y);
}
int ask_sum(int x,int y) {
makeroot(x);
expose(y); splay(y);
return sum[y];
}
int ask_dis(int x,int y) {
makeroot(x);
expose(y); splay(y);
return size[y];
}
void change_point(int x,int z) {
makeroot(x);
v[x]=z;
update(x);
}
void cal(int x,int a,int b) { //*a +b
if (!x) return;
v[x]=v[x]*a+b; //不要忘了维护单点值
sum[x]=sum[x]*a+size[x]*b;
ad[x]=ad[x]*a+b;
mul[x]=mul[x]*a;
}
//void push(int bh) {
// if (!bh) return;
// if (rev[bh]) {
// ...
// }
// if (ad[bh]!=0||mul[bh]!=1) {
// if (ch[bh][0]) cal(ch[bh][0],mul[bh],ad[bh]);
// if (ch[bh][1]) cal(ch[bh][1],mul[bh],ad[bh]);
// }
// mul[bh]=1; ad[bh]=0;
//}
void add(int x,int y,int z) {
makeroot(x);
expose(y); splay(y);
cal(y,1,z);
}
void multi(int x,int y,int z) {
makeroot(x);
expose(y); splay(y);
cal(y,z,0);
}