思路来源
https://www.luogu.com.cn/blog/flashblog/solution-p3690
https://www.cnblogs.com/19992147orz/p/8206693.html
https://www.cnblogs.com/candy99/p/6271344.html
前置知识
splay:真正理解rotate、会区间翻转
树链剖分:轻重链的概念
心得
杭电多校LCT的题,被120队过,不学新知识不行了,哎……
强推第一篇洛谷题解,有图可供食用,感觉还是挺好的……
然而,蒟蒻还是学了4个多小时……
access(x)
打通x到当前根的路径,
打通之后,x和根在同一Splay里,且x是深度最大的点,
即把x到根的这几棵Splay直接的虚边变为实边,原先对应的实边改为虚边
具体操作:
先把x旋到根,Splay(x),
并把x的右子树置空,c[x][1]=0,强行切断x和比x深度更大的点的连接
由于x少了一个右子树,更新x这个根节点维护的信息,pushup(x)
x从虚边跳到深度更浅的一棵Splay,不妨令y=f(x),则跳到了y这个点
重复这个过程,把y旋到根,Splay(y),
把y的右子树置空,变原实边为虚边,c[y][1]=0,表示切断y与y原来的右子树之间的连接,
将右子树置为上一个操作的Splay的根,变原虚边为实边,c[y][1]=x,表示连接y这棵Splay与x这棵Splay
事实上,由于是赋值语句,可以忽略置空这个操作
由于y换了一个右子树,更新y这个根节点维护的信息,pushup(y)
y跳到深度更浅的一棵Splay,重复上述过程,直至跳到原树的根
inline void access(int x){
for(int y=0;x;y=x,x=f[x])
splay(x),c[x][1]=y,pushup(x);//儿子变了,需要及时上传信息
}
makeroot(x)
换根,把x定为原树的根,
实际上,先打通x与原来的根rt的Splay,access(x)
然后,将这棵Splay打一个区间翻转的标记,pushr(x),
r[x]懒惰标记:下放时,x的儿子ls、rs的左右子树应当被交换,且其标记应翻转
由于原来的Splay是按深度增序维护的,
x作为深度最大的点,被Splay到根之后,x没有右子树,
打一个区间翻转的标记之后,左右子树互换,x最终没有左子树,
这表明x是这棵Splay里深度最小的点,起到了"本末倒置"的作用
相当于换了一个节点,将整棵树拽出来,有种提灯笼的感觉
inline void pushr(int x){//Splay区间翻转操作
swap(c[x][0],c[x][1]);
r[x]^=1;//r为区间翻转懒标记数组
}
inline void makeroot(int x){
access(x);splay(x);
pushr(x);
}
findroot(x)
找到x所在的原树的树根,
主要用于判断x和y两个点是否在一棵树上,即连通性
先打通x到根rt,access(x)
再将x旋到根,splay(x),保证复杂度
不断向左子树找,并沿途下放x的标记,保证x的左子树是翻转后的,
直至找到最左的点,这是深度最小的点,记为y,
splay(y),并返回y的值
inline int findroot(R x){
access(x); splay(x);
while(c[x][0])pushdown(x),x=c[x][0];
//如要获得正确的原树树根,一定pushdown!详见下方update(关于findroot中pushdown的说明)
splay(x);//保证复杂度
return x;
}
split(x,y)
将(x,y)在树上的路径,放到一个Splay里当实链
树上问题,常询问(x,y)点对之间的权值和/异或和之类,需要顾及两条链
先把其中一个点,不妨为x,换为根,makeroot(x)
再把y到根的Splay打通,access(y),y是深度最大的点
把y旋到根,splay(y),这样x及其余点都在y左子树里
inline void split(int x,int y){
makeroot(x);
access(y);splay(y);
}
link(x,y)
连一条x-y的边,注意判断树上x-y间是否已经有边,
先把x指定为根,makeroot(x),
再判一下x和y的连通性即可,findroot(y)==x,若连通再连边就成环了
否则说明x和y在两棵树上,令x这棵树引一条虚边到y即可,f[x]=y
inline bool link(int x,int y){
makeroot(x);
if(findroot(y)==x)return 0;//两点已经在同一子树中,再连边不合法,如果保证连边一定合法,则可省略本行
f[x]=y;
return 1;
}
cut(x,y)
断开x-y在原树上的边,动态删边
①保证删边合法
先扒出(x,y)在树上的路径,split(x,y),注意split里的操作
先makeroot(x),此时x是原树的根,
再access(y),打通y到x的路径,Splay里只有y到x的路径,
再Splay(y),此时y在Splay里是根,
则x由于与y相连,深度比y小1,一定是y的直连左子树,
直接令f[x]=0,表示x的父亲不存在;
c[y][0]=0,表示y的左子树不存在,并更新y的信息,pushup(y)
inline void cut(int x,int y){
split(x,y);
f[x]=c[y][0]=0;
pushup(y);//少了个儿子,也要上传一下
}
②删边不一定合法,即不能保证原树上x-y之间有边
先将x钦定为根,makeroot(x)
(1)判断一下y和x的连通性,若findroot(y)!=x,表示y和x在原树上不连通,无边,否则转(2)
(2)注意到(1)中执行了findroot(y),其中包含access(y)和splay(y)和splay(x),
现在x和y的路径被打通,x是Splay的根,Splay中只有x到y的路径,
若y在Splay中的直连父亲不是y,f[y]!=x,说明y和x之间还有别的点,否则转(3)
(3)此时f[y]==x已经成立,但可能y是x的右子树,y还有左子树,导致中序遍历时xy之间还有点,
所以如果ch[y][0]!=0,说明y和x之间还有别的点,否则说明这是合法的情况
inline bool cut(int x,int y){
makeroot(x);
if(findroot(y)!=x||f[y]!=x||c[y][0])return 0;
f[y]=c[x][1]=0;//x在findroot(y)后被转到了根
pushup(x);
return 1;
}
合法的情况:x、y的Splay里只有x、y两个点,x为根,y为x的右子树
所以,如果维护了子树的size信息,先判连通,判完连通之后,
x为原树根且为Splay的根,如果sz[x]==2则合法,否则不合法
inline bool cut(int x,int y){
makeroot(x);
if(findroot(y)!=x||sz[x]>2)return 0;
f[y]=c[x][1]=0;
pushup(x);
return 1;
}
nroot(x)
not root的缩写,用于判断x是不是Splay的根,返回1代表不是Splay的根
f[x]的含义:
如果x不是Splay的根,f[x]=z是实边,代表x在Splay里的父亲z
如果x是Splay的根,记x所在的Splay里深度最小的点为y,f[x]=z是虚边,代表y在原树中的父亲z
ch[x][0/1]的含义:
仅用于同一棵Splay的点的左右孩子,可以认为是实边的反向边
inline bool nroot(R x){//判断节点是否为一个Splay的根(与普通Splay的区别1)
return c[f[x]][0]==x||c[f[x]][1]==x;
}//原理很简单,如果连的是轻边,他的父亲的儿子里没有它
个人体会
LCT:Link-Cut Tree,动态树
像树剖分轻重链一样,动态树维护的是一个森林,森林上有虚链和实链
每个点与Splay里的点号一致,共n个点,
每个点往下只有一条链是实链,剩下全是虚链
①每个点恰好出现在一个Splay里
②一个Splay按深度关键字维护一条实链,即中序遍历这棵Splay,可以按深度增序访问这条实链
虚链的边:实际上指向的是这棵Splay里编号最小的点在原树中的父亲,只能从子找到父亲,认父不认子
实链的边:一棵Splay内部的边,既能通过f[]找父亲,也能通过ch[][0/1]找儿子
③由于Splay的根还有父亲,即虚链边对应的父亲,
所以Splay中rotate的时候,考虑x y z的时候,x的父亲是y,y的父亲是z,
z可以为虚链边的父亲,但这种情况下仍需特判,因为此时z不参与旋转,
这样,设换根之前根是x,虚链点是z,f[x]=z,则换根后设根是y,必有f[y]=z
板子整理
以洛谷P3690 动态树模板为例
#include<bits/stdc++.h>
#define lc c[x][0]
#define rc c[x][1]
using namespace std;
const int N=3e5+9;
//f:父亲 c:儿子 v:单点值 s:子树值 stk:用于从上到下释放标记的栈 r:区间翻转标记
int n,m,f[N],c[N][2],v[N],s[N],stk[N];
bool r[N];
bool nroot(int x){//判断节点是否为一个Splay的根(与普通Splay的区别1)
return c[f[x]][0]==x||c[f[x]][1]==x;
}//原理很简单,如果连的是轻边,他的父亲的儿子里没有它
void pushup(int x){//上传信息
s[x]=s[lc]^s[rc]^v[x];
}
void pushr(int x){int t=lc;lc=rc;rc=t;r[x]^=1;}//翻转操作
void pushdown(int x){//判断并释放懒标记
if(r[x]){
if(lc)pushr(lc);
if(rc)pushr(rc);
r[x]=0;
}
}
void rotate(int x){//一次旋转
int y=f[x],z=f[y],k=c[y][1]==x,w=c[x][!k];
if(nroot(y))c[z][c[z][1]==y]=x;c[x][!k]=y;c[y][k]=w;//额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
if(w)f[w]=y;f[y]=x;f[x]=z;
pushup(y);
}
void splay(int x){//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
int y=x,z=0;
stk[++z]=y;//st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
while(nroot(y))stk[++z]=y=f[y];
while(z)pushdown(stk[z--]);
while(nroot(x)){
y=f[x];z=f[y];
if(nroot(y))
rotate((c[y][0]==x)^(c[z][0]==y)?x:y);
rotate(x);
}
pushup(x);
}
void access(int x){//访问
for(int y=0;x;x=f[y=x]){
splay(x),rc=y,pushup(x);
}
}
void makeroot(int x){//换根
access(x);splay(x);
pushr(x);
}
int findroot(int x){//找根(在真实的树中的)
access(x);splay(x);
while(lc)pushdown(x),x=lc;
splay(x);
return x;
}
void split(int x,int y){//提取路径
makeroot(x);
access(y);splay(y);
}
void link(int x,int y){//连边
makeroot(x);
if(findroot(y)!=x)f[x]=y;
}
void cut(int x,int y){//断边
makeroot(x);
if(findroot(y)==x&&f[y]==x&&!c[y][0]){
f[y]=c[x][1]=0;
pushup(x);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
scanf("%d",&v[i]);
}
while(m--){
int type,x,y;
scanf("%d%d%d",&type,&x,&y);
switch(type){
case 0:split(x,y);printf("%d\n",s[y]);break;
case 1:link(x,y);break;
case 2:cut(x,y);break;
case 3:splay(x);v[x]=y;//先把x转上去再改,不然会影响Splay信息的正确性,子树都是对的,x及以上的点会先pushup(x)
}
}
return 0;
}