LCT学习笔记
实现
从例题开始:
对于一棵静态的树,常见方法是树剖然后走链,但是在动态的情况下常见的重链或长链就会很慢,因为修改连边情况后就不满足性质了
引入一个新的方法:实链剖分,对于一个节点,任选一个儿子,连边为实边,其余为虚边,注意这里的实边是可以变化的
但是显然这样剖分的形式就不固定了,所以之前线段树维护的方法就不成立了,但是可以用 splay 解决
此时树变成了若干条实边和虚边,连接在一起的实边成为实链
对每条实链进行维护,使得 splay 的中序遍历为原树上深度从小到大排序的结果
对于虚边,作用是把这些 splay 连起来,方式如下:
-
找到该 splay 深度最小的节点,记为 k,若为根则不管
-
找到它的 fa,由它向当前 splay 的根连边
因为一个节点会有多个儿子,但是它只会存一个,就是认父不认子
区别于普通 splay,因为实链之间不能相互影响,操作时还需要判断是否为当前 splay 的根,如果是,返回-1,rotate 和 splay 函数区别不大
access:就是把点 x 到根路径上的点全部变成实边
首先将 x 转到当前 splay 的根,然后将 x 与 fa 之间的边变成实边,就是放到右儿子,然后处理 fa 所在子树,重复此操作直到整棵树的根
同时因为将 x 到根打包成一个 splay,所以 x 原来的实儿子变成虚儿子
void access(int x)
{
int t=0;
while(x)
{
splay(x);
tr[x].rs=t;
pushup(x);
t=x,x=tr[x].f;
}
}
makeroot:将 x 节点设为所在联通快的根
首先打通 x 到根的路径,此时 x 一定在这个子树的中序遍历的最后一位
如果我们将它设为根,那么它就是深度最小的点,但是他的 fa 作为深度第二大的点,理应在倒数第二位,翻转后会在第二位,这部分可以打懒标记实现
所以最后就是打通 x 到根的路径,然后把 x 转到根,给 x 打标记
同时将 x 转到根时,路径上的标记要全部下放
void makeroot(int x)
{
access(x);
splay(x);
swap(tr[x].ls,tr[x].rs);
if(tr[x].ls) tr[tr[x].ls].lazy^=1;
if(tr[x].rs) tr[tr[x].rs].lazy^=1;
}
split:就是把 x 到 y 的路径变成实链,然后分成新的 splay,操作上就是先把 x 换到整棵树的根,然后打通 y 到根的路径,最后将 y splay 到子树的根
findroot:就是找到 x 所在点的实链的根,可以先将 x 转到根,此时因为根深度最小,暴力往左边找,注意下放标记,最后要转回去
link:连边,将 x 换到根,然后判断 y 和 x 是否在同一个联通快,不在就连一条虚边
cut:断边,先判断是否在一个联通快内,然后将路径独立出来,此时 y 若与 x 相连,则 y 在 x 的父亲且中序遍历上相邻,所以 x 不能有右子树
最后整道题就是这样的:
点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,m;
struct node{
int f,ls,rs,val,sum,lazy;
}tr[N];
void pushup(int p)
{
tr[p].sum=tr[tr[p].ls].sum^tr[tr[p].rs].sum^tr[p].val;
}
void pushdown(int x)
{
if(tr[x].lazy)
{
swap(tr[x].ls,tr[x].rs);
if(tr[x].ls) tr[tr[x].ls].lazy^=1;
if(tr[x].rs) tr[tr[x].rs].lazy^=1;
tr[x].lazy=0;
}
}
int get(int x)
{
if(tr[tr[x].f].ls==x) return 0;
if(tr[tr[x].f].rs==x) return 1;
return -1;
}
void change(int x,int f,int k)
{
tr[x].f=f;
if(k==1) tr[f].rs=x;
if(k==0) tr[f].ls=x;
}
void rotate(int x)
{
int y=tr[x].f,z=tr[y].f;
int k=get(x),k1=get(y);
int u=0;
if(k==1) u=tr[x].ls;
if(!k) u=tr[x].rs;
change(u,y,k),change(y,x,k^1),change(x,z,k1);
pushup(y),pushup(x);
}
void pushall(int x)
{
if(get(x)!=-1) pushall(tr[x].f);
pushdown(x);
}
void splay(int x)
{
pushall(x);
while(get(x)!=-1)
{
int y=tr[x].f;
if(get(y)!=-1)
{
(get(x)^get(y))?rotate(x):rotate(y);
}
rotate(x);
}
}
void access(int x)
{
int t=0;
while(x)
{
splay(x);
tr[x].rs=t;
pushup(x);
t=x,x=tr[x].f;
}
}
void makeroot(int x)
{
access(x);
splay(x);
swap(tr[x].ls,tr[x].rs);
if(tr[x].ls) tr[tr[x].ls].lazy^=1;
if(tr[x].rs) tr[tr[x].rs].lazy^=1;
}
void split(int x,int y)
{
makeroot(x);
access(y),splay(y);
}
int findroot(int x)
{
access(x);
splay(x);
while(tr[x].ls) pushdown(x),x=tr[x].ls;
splay(x);
return x;
}
void link(int x,int y)
{
makeroot(x);
if(findroot(y)==x) return;
tr[x].f=y;
}
void cut(int x,int y)
{
if(findroot(x)!=findroot(y)) return;
split(x,y);
if(tr[x].f!=y||tr[x].rs) return;
tr[x].f=tr[y].ls=0;
pushup(y);
return;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
tr[i].sum=tr[i].val=x;
}
for(int i=1;i<=m;i++)
{
int opt,x,y;
scanf("%d%d%d",&opt,&x,&y);
if(opt==0)
{
split(x,y);
printf("%d\n",tr[y].sum);
}
if(opt==1) link(x,y);
if(opt==2) cut(x,y);
if(opt==3) splay(x),tr[x].val=y;
}
return 0;
}
例题
1. [国家集训队] Tree II
难点在于有两个标记,加法和乘法的下放是有一定顺序的,下放乘法标记时原有的增量也会乘上这个数
所以每次可以先下放乘法,再处理加法,因为每个数都要加,所以还要另外记录 siz
最后把链分离出来输出和即可
2. [Wc2006]水管局长数据加强版
首先不强制在线,所以可以倒序处理变成加边
显然的,最后的答案一定在最小生成树上,所以要动态维护图的最小生成树
考虑新加入的边 \((u,v)\),如果未联通则直接建边,否则找到链上最大的边,判断大小关系,决定是否连边
因为是涉及边的问题,所以可以把每条边拆成点,维护最大值和位置即可
3. [Codechef MARCH14] GERALD07加强版
在一张没有环的图上,联通块个数就是 n- 边数
考虑我们新加进来一条边,此时如果这两个点已经连通,那么去掉环上的一条边联通情况不变
所以每一次可以删掉加入时间最早的边,因为要区间查询,所以主席树维护一下加到每一条边的时候每条边的存在情况即可
4.[bzoj3159]决战
难点在于反转,因为如果直接翻转这条链。那么子树的位置关系也会改变,这样就改了整颗子树,显然是不对的
如何做到只改这条链,可以类似文艺平衡树的方法,把修改的部分在 splay 上弄成一个区间,这样直接分裂下来打懒标记即可
所以可以对每个实链维护一个 splay,然后 access 操作时进行分裂和合并,两棵树同时操作即可

浙公网安备 33010602011771号