洛谷 P3690 【模板】Link Cut Tree (动态树)

LCT 动态树模板

n n n 个点和每个点的权值,处理以下操作:

  • 0 x y 查询 xy 之间的点权的 x o r \mathrm{xor} xor
  • 1 x y 连接 x,y ,若已经联通则不用再连接
  • 2 x y 删除 x,y ,不保证边存在
  • 3 x yx 的点权变成 y

参考:https://www.cnblogs.com/flashhu/p/8324551.html

LCT 用 Splay 动态维护一个森林,目的是用平衡树来表示不平衡树,使得所有操作的复杂度都是 O ( log ⁡ n ) O(\log{n}) O(logn)

其核心操作是 access(x) ,作用是使 x 到其根节点上的路径成为一条实链。具体地:

void access(int x){
    int y = 0; // 初始需要赋值的右孩子是 0 (None)
    while(x){
        splay(x); // 首先把 x splay 到顶端
        // 一棵树被划分为多个 splay 此处的操作是在 splay 中的操作
        // c[x][1] 表示 splay 中 x 的右孩子,
        // splay 的中序遍历要求深度递增,
        // 而现在 x 在 splay 顶端,其右孩子(若存在)
        // 必定比 x 的深度要深,这样是不符合 LCT 中 splay 定义的
        // 因此在第一次操作时,要将 x 的右孩子置空
        // 并且注意:此处置空的是 splay 中的右孩子
        // 而在原树中 x 的孩子的 path parent 仍然是 x
        // 也就是说 f[c[x][1]]=x (置空前)
        c[x][1]=y; // 对于第一次来说,此处是将 x 的右孩子置空
        pushup(x); // 更新信息
        // 此时 x 已经到当前 splay 的顶端
        // 下一步是到当前 splay 的 path parent 那里去,继续向上延长实链
        // f[x] 即是当前 splay 的 path parent
        // 但是在原树中 f[x] 不一定是 x 的父亲
        // 它是当前 splay 中深度最小的那个结点的父亲
        // 也就是最靠左的那个结点的父亲
        // 沿着 path parent 到了上一个结点(比如 w)之后
        // 再重复执行 splay(w) ,即将 w 变成其 splay 中的顶端结点
        // 并使得 w 的右孩子变成结点 x 
        // 此时便完成了实链的延申, w 所在的 splay 和 x 所在的 splay 也就接了起来
        // 所以下句话 y = x 就是要存下当前的结点
        // 用以上面 splay 的延申
        // 依次重复,直到当前结点的 path parent 为 0
        // 也就是说当前结点已经变成了根节点
        // 此时该 spaly 的根节点为原树的根
        // 最右侧结点,也就是深度最大的结点是 x (第一次的 x)
        // 并且 x 没有右孩子(在 splay 中)
        y=x; // 使下次操作的右孩子可以指向当前结点
        x=f[x]; // 下一个点是当前的 path parent 
    }
}

makeroot(x) 使得 x 变成原树中的根,方法是首先 access(x) ,建立从根节点到 x 的 splay ,然后 splay(x) ,此时 x 在 splay 的顶端,由于 x 是该 splay 中最深的点,因此这时 x 是没有右子树的,所以考虑直接翻转 splay ,使得 splay 中所有结点的深度翻转,使得 x 成为深度最小的结点,也就变成了根节点。

void pushr(int x){
	swap(c[x][0],c[x][1]); // 直接交换 x 的左右孩子
	r[x]^=1; // 懒惰标记,剩下的子树的孩子们现在还不用翻转
}
void makeroot(int x){
	access(x); // 首先建立根到 x 的实链
	splay(x); // x 移到 splay 的顶端
	pushr(x); // 翻转 splay ,使 x 变成根
}

findroot(x) 用来找 x 在原树中的根,首先 access(x) ,建立 x 到根的一条实链通路,此时 x 和原树的根就在一个 splay 里啦,然后再 splay(x),使得 x 位于 splay 的顶端,由于 splay 中深度从左到右是递增的,因此 x 必定没有右子树,根节点必定在 splay 的最左侧,所以要一直找左子树的左孩子。找到之后再 splay 使得根节点在 splay 的顶端。

void pushdown(int x){
	if(r[x]){
		if(lc)pushr(lc);
		if(rc)pushr(rc);
		r[x]=0;
	}
}
int findroot(int x){
	access(x); // 首先建立 x 到根之间的实链通路
	splay(x); // 使 x 在 splay 的顶端
	while(c[x][0]){ // 一直找左子树
		pushdown(x); // 向下更新翻转信息
		x=c[x][0];
	}
	splay(x);
	return x;
}

split(x,y) 的作用是在 xy 之间拉一条实链:

void split(int x,int y){
	makeroot(x); // 将 x 作为原树的根
	access(y); // 拉一条从根(x)到 y 的实链
	splay(y); // 令 y 作为 splay 的顶端(splay 的根)
}

link(x,y) 是将 x 连到 y 上,即 y 作为 x 的父亲:

bool link(int x,int y){
	makeroot(x); // 使 x 作为根节点
	if(findroot(y)==x)return 0; // 已经在同一原树内,不再连;如果保证连边合法的话这句话可以删掉
	f[x]=y; // x 的父亲为 y (其实是 path parent 是 y,也就是一条虚边)
	return 1;
}

cut(x,y) 是将 xy 的连边断开,保证断边合法时:

void cut(int x,int y){
	split(x,y); // 首先xy之间拉一条实链,此时 y 是 splay 的根,x 是原树的根,在 splay 中 x 必定是 y 的左儿子,在原树中,x 必定是 y 的父亲(path parent)
	f[y]=c[y][0]=0; // 断开 x 和 y 之间的边
	pushup(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);
    }
}

代码如下:

#include<iostream>
#include<cstdio>
#define MAXN 300009
#define lc c[x][0]
#define rc c[x][1] 
using namespace std;
int n,m,t,x,y,f[MAXN],c[MAXN][2],v[MAXN],s[MAXN],st[MAXN];
bool r[MAXN];
bool nroot(int x){//whether x isn't the root of splay
    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(w)f[w]=y;f[y]=x;f[x]=z;
    pushup(y);
}
void splay(int x){
    int y=x,z=0;
    st[++z]=y;//stack, stores the whole path from cur point to root
    while(nroot(y))st[++z]=y=f[y];
    while(z)pushdown(st[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(){
#ifdef WINE
    freopen("data.in","r",stdin);
#endif
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&v[i]);
    while(m--){
        scanf("%d%d%d",&t,&x,&y);
        if(t==0){split(x,y);printf("%d\n",s[y]);}
        if(t==1)link(x,y);
        if(t==2)cut(x,y);
        if(t==3){splay(x);v[x]=y;}
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值