LCT详解
引入
思考对于一棵树,如果我们需要维护这棵树上的某些信息,如一条链上的最大值,权值和等,我们可以使用树链剖分很好的维护。但是我们如果要求解一个树上的异或和,我们倘使使用线段树维护,则树链剖分的时间复杂度高达 n l o g 2 n nlog^{2}n nlog2n,这个效率可谓及其的低下,稍不注意就可能超时。而且如果我们的树是动态的(比如说我们需要在两点之间加边或删边),我们的树链剖分就没有任何办法了,在这个时候,我们就要使出我们的动态树了
LCT
动态树Link-Cut Tree,简称 L C T LCT LCT。由他的名字我们可以知道,这个数据结构可以实现连接(Link)与切断(Cut),并且几乎可以完成所有树链剖分的操作。需要注意的一点是, L C T LCT LCT并不叫动态树,他只是动态树的一种。还有其他的动态树。
例题
P3690 【模板】动态树(LCT)
下面,我将为你逐步分解这道例题
结构体定义
我们先要定义好我们的结构体,先把
L
C
T
LCT
LCT的原理放一放。现在你只需要知道
L
C
T
LCT
LCT是用
S
p
l
a
y
Splay
Splay实现的即可,所以LCT的结构体和
S
p
l
a
y
Splay
Splay几乎一致。
Code:
struct lit{
ll val,id,sum;//sum维护异或和
ll rev;//rev是区间翻转的标记,下面会解释
lit *l,*r,*p;
lit(ll i,ll v){
id=i;sum=val=v;rev=0;
rev=0;
l=r=p=nullptr;
}
};
LCT的基本原理
LCT中有两个基本的概念:偏好路径和偏好儿子。
我们先明确一下我们的
S
p
l
a
y
Splay
Splay维护的是什么顺序???我们是以深度维护的整个
S
p
l
a
y
Splay
Splay。
思考对于一个树上的节点,他可能有很多个儿子,但他在
S
p
l
a
y
Splay
Splay中只可能有一个右儿子,所以我们要引入偏好儿子来维护他。
显然我们的偏好儿子是可以认他的父亲的,他的父亲也要认偏好儿子,那其他儿子呢?对于其他儿子,我们不可能父认子,那就只能子认父了,这也是一个重要的思想,就是只记录你要求的路径上的值,不管其他的路径。
splay(伸展)
由于我们用到了
S
p
l
a
y
Splay
Splay,那肯定少不了旋转与伸展。
我们需要将节点
u
u
u伸展到根。在
S
p
l
a
y
Splay
Splay模板中,我们是要将
u
u
u节点伸展到整棵树的根,但是在这里,我们只需要将其伸展到其所在偏好路径的根即可。注意一点,我们由于要区间翻转(理由下面说),我们在进行伸展之前,要下传标记。
Code:
inline bool isRight(lit *u){//判断u是不是右儿子
if(!u)return 0;
return u->p&&u->p->r==u;
}
inline bool isRoot(lit *x){//判断u是否为根节点
if(!x)return 0;
return !x->p||(x!=x->p->l&&x!=x->p->r);
//没有父亲节点代表他为整个根,父亲节点不认这个儿子说明u与其父亲间为轻边
}
inline ll getsum(lit *u){return u?u->sum:0;}
inline void pushup(lit *u){
if(!u)return ;
u->sum=u->val;
u->sum^=getsum(u->l)^getsum(u->r);
}
inline void mark_co(lit *u){
if(!u)return ;
u->rev^=1;
}
inline void pushdown(lit *u){
if(!u||!u->rev)return;
swap(u->l,u->r);
mark_co(u->l);mark_co(u->r);
u->rev=0;
}
inline void rotate(lit *u){//splay的旋转
if(!u||!u->p)return ;//没有就不旋转
lit *p=u->p;
lit *g=p->p;
pushdown(p);pushdown(u);//记得下先下传p,再u
if(p->l==u){//u为左儿子
p->l=u->r;//由上往下更新
if(u->r)u->r->p=p;
u->r=p;
}
else{
p->r=u->l;
if(u->l)u->l->p=p;
u->l=p;
}
p->p=u;
u->p=g;
if(g){//最后更新g
if(g->l==p)g->l=u;
else if(g->r==p)g->r=u;
}
pushup(u);pushup(p);
}
inline void splay(lit *x){//伸展
if(!x)return ;
vector<lit*> stack;
lit *u = x;
while (u) {//首先将从u到根的节点都下传
stack.push_back(u);
u = u->p;
}
while (!stack.empty()) {
pushdown(stack.back());
stack.pop_back();
}
while (!isRoot(x)) {//一直旋转到根
lit *p= x->p;
if (!isRoot(p)) {//p不为根则双旋
if (isRight(x)==isRight(p))rotate(p);//同侧先旋p
else rotate(x);//异侧先旋为同侧
}
rotate(x);
}
pushup(x);//记得更新
}
accesss(连接)
a
c
c
e
s
s
access
access是
L
C
T
LCT
LCT的根本,后续我们地操作基本都是以他为基础。
a
c
c
e
s
s
access
access的作用是打通一条从
u
u
u到树根的偏好路径,注意是树根。这个操作虽然很重要,但是没什么好说的。上文我们已经说了
L
C
T
LCT
LCT 子认父的性质,那我们只需要从
u
u
u一直往上跳即可。由于偏好路径父也要认子,所以我们要注意改变父节点的儿子。
Code:
inline void access(lit *u){//打通u到根的偏好路径
if(!u)return ;
lit *y=nullptr;//y记录儿子
while(u){
splay(u);//先将u伸展到根
u->r=y;//由于y的深度大于u,则y只可当u的右儿子
if(y)y->p=u;//连边
pushup(u);
y=u;
u=u->p;//u向上跳
}
}
makeroot(设置为根节点)
讲完
a
c
c
e
s
s
access
access操作,下面来讲几个我们会用到的操作。
m
a
k
e
r
o
o
t
makeroot
makeroot这个操作是将
u
u
u节点设为整棵树的根节点。这个操作怎么实现呢?我们画个图方便理解一下:

我们把
x
x
x变为根后图如下:

我们发现对于其他部分,他们没有任何关系。对于rt到x的路径,他们的相对深度反了过来,那我们直接将这个区间翻转即可,这就是我们翻转标记的作用。
到这里代码就很显然了。
Code:
inline void makeroot(lit *u){//将u变为整棵树的根
if(!u)return ;
access(u);//先将u到根的偏好路径打通,则此时u必定在当前偏好路径的最后一个
//变为根后,原来的根深度最低,u最高,其他的顺序相反,所以直接翻转即可
splay(u);
u->rev^=1;
}
findroot(查找树的根)
f i n d r o o t findroot findroot是用来查找 u u u的根的。想要实现这个操作,首先我们肯定先打通偏好路径在将 u u u伸展到根。由于根节点的深度是最低的,所以我们只需要一直往左边跑即可。记得下传标记!
inline lit *findroot(lit *x){//查找x所在根
if(!x)return nullptr;
access(x);//先打通偏好路径
splay(x);
while(x->l){pushdown(x);x=x->l;}//根节点深度最低,在最左边
splay(x);
return x;
}
split(分裂某段路径)
这个操作是为了路径查询用的。他可以将路径
(
x
,
y
)
(x,y)
(x,y)的值保存在
y
y
y上。
我们直接先将
x
x
x设为根,在打通
x
x
x到
y
y
y的路径,最后将
y
y
y伸展到根即可。
Code:
inline void split(lit *x,lit *y){//分裂出x-y的路径,y为其的根
makeroot(x);//先将x变为根
access(y);//在打通y到x的偏好路径,此时y-x为一个splay
splay(y);
}
link(连边)
讲了这么久,终于可以开始操作了。
l
i
n
k
link
link是
L
C
T
LCT
LCT的基本操作。
我们在进行操作之前先要开一个数组,记录对应点在
S
p
l
a
y
Splay
Splay中所对应节点。
我们先将
x
x
x设为根,为了防止边建重了,我们先要判断一下
y
y
y的根是否为
x
x
x,如果不是则建边。但是,要注意,在这了我们建的是轻边,及非偏好路径。所以我们只需要子认父即可。
由于x为根,所以只需要
x
x
x认
y
y
y即可。
Code:
inline void build(ll n){//建树
node.resize(n+1,nullptr);
for(ll i=1;i<=n;i++)node[i]=new lit(i,w[i]);
}
inline void link(ll y,ll x){//连接x-y,反向连接玄学加速
if(x<0||x>=(ll)node.size()||!node[x]||y<0||y>=(ll)node.size()||!node[y])return;
//排除x,y不存在的情况
makeroot(node[x]);//先将x变为根
if(findroot(node[y])!=node[x])node[x]->p=node[y];
//倘使y的根不为x,则建立轻边
}
cut(删边)
对于删边操作,我们肯定也先要将 x x x变为根,在打通 y y y到 x x x的路径后,将 y y y旋到根,若 x , y x,y x,y之间有边,则此时 x x x必定为 y y y的左儿子,因为 x x x的深度小于 y y y。此时我们只需要将 x − > p x->p x−>p与 y − > l y->l y−>l变为空指针即可。注意,此时我们一定要 p u s h u p ( y ) pushup(y) pushup(y),因为 y y y的子树改变了。
inline void cut(ll x,ll y){//删除边x-y
if(x<0||x>=(ll)node.size()||!node[x]||y<0||y>=(ll)node.size()||!node[y])return;
makeroot(node[x]);//也是先将x设为根
access(node[y]);splay(node[y]);
if(node[y]->l==node[x]){
//若此时y的根为x,且y为x的子节点
node[x]->p=nullptr;node[y]->l=nullptr;
pushup(node[y]);
}
}
change(单点修改)
对于一个点,我们只需要将他伸展到根,然后直接修改即可
void change(ll x,ll v){
if(x<=0||x>(ll)node.size()||!node[x])return ;
splay(node[x]);node[x]->val=v;pushup(node[x]);
}
query(查询)
只需要将查询的边用 s p l i t split split函数分出来,再直接查询答案即可。
ll query(ll x,ll y){
if(x<=0||x>(ll)node.size()||!node[x]||y<=0||y>(ll)node.size()||!node[y])return 0;
split(node[x],node[y]);
return getsum(node[y]);
}
总结
L C T LCT LCT的基础学习就到这里,真正难的不是数据结构,而是对数据结构的使用。

879

被折叠的 条评论
为什么被折叠?



