学习笔记——LCT

前言#

这太难了啦~但是冬令营讲这个东西了,提前开坑。前置芝士

Define#

#define ls tr[x].ch[0]
#define rs tr[x].ch[1]
struct Tree{int ch[2],val,fa,rev,xv;}tr[MAXN];

LCT?#

LCT 是怎么超越一般的树剖与平衡树从而达到维护一个森林的效果的呢?

LCT 经过实链剖分,再加上 Splay 辅助树,使得其拥有一些性质:

  1. 每个 Splay 中维护的树中序遍历在原树中是一条链,深度是递增的。因此 Splay 中的前驱与后继就是原树中的父亲和儿子。
  2. 每个节点被包含且仅包含在一个 Splay 中。
  3. 各个 Splay 之间用虚边相连。所谓虚边,实际上就是从儿子能单向地找到父亲但是父亲却找不到儿子,因为父亲只认经过实边的那个唯一的儿子。

注意,以下所有操作都是针对与原树的,辅助树只是用来辅助的。

Access#

LCT 的基本操作只有一个就是 accessaccess(x) 表示将点 x当前指定的根节点之间的路径上的边全部变成实边,相当于把 x 与根节点放到同一棵 Splay 中,从而打通它们之间的路径。并且 x 是这条实链上最后一个节点

其过程是从 x 当前所在的 Splay 开始,每次将 x 旋到这个 Splay 的根,然后将其父亲的边化实(实际上就是让父亲来认这个儿子),然后对 x 当前的父亲进行同样的处理,直到处理到原树当前指定的根节点为止。懒得放图了,看 FlashHu 的博客去。

void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}

但是要注意,经过上述操作之后,所得的 Splay 并不平衡,所以一般在 access(x) 后会加上 splay(x)

Makeroot#

顾名思义,使 x 成为根,即把 x 指定为当前原树的根。我们考虑 LCT 第一条性质,想要 x 实原树的根,那么在辅助树中,x 就要在根所在的 Splay 中序遍历的第一个。于是我们想到对 x 进行一个 access(x),这样 x 就变成了其中序遍历的最后一个(因为中序遍历反映的是原树上某条实链的深度递增序列,access(x) 之后,x 自然就是根所在的实链深度最深的一个节点)。为了使之变成第一个,我们对其所在的 Splay 进行翻转,这样中序遍历就成了第一个,x 在原树中就成了深度最小的一个节点了(就是根啊)。

void pushr(int x){swap(ls,rs);tr[x].rev^=1;}//文艺平衡树大家都会吧
void makeroot(int x){access(x);splay(x);pushr(x);}

Findroot#

即找到 x 所在的原树的根(LCT 是维护森林的)。通过对 Makeroot 的思考,我们容易想到希望把 x 放到与根同一个 Splay 中。然后根实际上就是这个 Splay 中序遍历的第一个,就不断找做儿子就行了。而对于一个查找,通常会通过 splay(x) 来保证复杂度。

int findroot(int x){access(x);splay(x);while(ls) pushdown(x),x=ls;splay(x);return x;}

Split#

用来访问 LCT 中的一条链。我们拥有 Makeroot 之后就能做很多事情了。首先让 x 变成根,然后把 yx 的路径打通,再把 y 通过 splay(y) 上来。此时,y 是这个 Splay 的根,这条链的信息是存在 y 中的

void split(int x,int y){makeroot(x);access(y);splay(y);}

xy 连一条边。先让 x 成为根,然后如果 x,y 不在同一个连通块中,就让 x 的父亲变成 y

void link(int x,int y){makeroot(x);if(findroot(y)!=x) tr[x].fa=y;}

Cut#

断掉 x,y 之间的边。这个比较复杂。如果 x,y 之间本就没有边,或者 x,y 之间有多个节点都是不合法的要求。

那先来考虑简单的,如果保证合法了,那很简单,我们直接把 x,y 之间的边搞出来,用 split(x,y) 即可。此时,y 是 Splay 的根,x 是其左儿子。原因很简单,我们在 split(x,y) 的时候,让 x 成为了根,然后把 y 旋到了 Splay 的根。所以,x 的中序遍历比 y 小,又恰好有边直接相连,那只能是根 y 的左儿子了。

void cut(int x,int y){split(x,y);tr[x].fa=tr[y].ch[0]=0;}

那如果不保证合法呢?首先还是 makeroot(x)

于是我们有一些判断来除去这些情况:

  1. 如果 x,y 不在同一个连通块中;
  2. 如果 y 的父亲不是 x,那说明他们之间还有其它点;
  3. 如果 Splay 中,y 还有左儿子,那说明中序遍历中,x,y 间夹了些点。

这些都是不合法的。

然后令 x 的右儿子和 y 的父亲为空,表示断开这条边。

为什么 y 就是 x 的右儿子?因为我们已经 makeroot(x) 了,此时如果 yx 直接相连,那么 y 必然是 x 的亲儿子中的一个。而在 findroot(y) 的时候,我们进行了 access(y),也就是 yx 在一棵 Splay 中了,然后又在找到 x 之后,把 x 旋到了根。那么如果 x,y 直接相连,y 只能是 x 的右儿子。

void cut(int x,int y){
	makeroot(x);
	if(findroot(y)!=x||tr[y].fa!=x||tr[y].ch[0]) return;
	tr[y].fa=tr[x].ch[1]=0;pushup(x);
}

Isroot#

注意这不是一个应用于原树的函数,这是表示 x 是否是 x 所在的 Splay 的根节点。判断其实非常简单,如果是根,那么它的父亲是不认它的。

为什么需要这个?

下面就有 LCT 里的 Splay 和一般的 Splay 不一样的地方。其中之一就是这个东西。由于有很多 Splay,所以我们可以从一棵 Splay 的根 u,通过 tr[u].fa 来得到另一棵 Splay 中的一个节点。这非常可怕,意味着如果没法判断 u 是不是根的话,整棵树都会被破坏掉。于是就有了这个东西。

bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}

关于 Splay#

大家都会写 Splay。但是这里的 Splay 有些许不同,但总体还是一样的。

先来看 Splay 的代码吧。

void pushup(int x){tr[x].xv=tr[ls].xv^tr[rs].xv^tr[x].val;}
void pushdown(int x){
	if(!tr[x].rev) return;
	if(ls)pushr(ls);if(rs)pushr(rs);tr[x].rev=0;
}
void rot(int x){
	int f=tr[x].fa,k=(x==tr[f].ch[1]),g=tr[f].fa,v=tr[x].ch[k^1];
	if(!isroot(f)) tr[g].ch[f==tr[g].ch[1]]=x;tr[x].ch[k^1]=f;tr[f].ch[k]=v;//diff
	if(v) tr[v].fa=f;tr[f].fa=x;tr[x].fa=g;
	pushup(f);
}int stk[MAXN],top;
void splay(int x){
	int tmp=x;top=0;stk[++top]=tmp;
	while(!isroot(tmp)) tmp=tr[tmp].fa,stk[++top]=tmp;
	while(top) pushdown(stk[top--]);//diff
	while(!isroot(x)){//diff
		int f=tr[x].fa,g=tr[f].fa;
		if(!isroot(f))
			rot((f==tr[g].ch[1])^(x==tr[f].ch[1])?x:f);
		rot(x);
	}pushup(x);
}

这里包含了挺多东西的,但是 pushuppushdown 大可不用管它。上面不同之处我都加了 //diff。接下来我逐一解释:

  1. 单旋的时候,由于我们不能让 Splay 的根的父亲来认这个虚边连着的儿子,所以我们在 rot 的时候要注意其父亲是不是根;
  2. 发现在 splay 操作主体前多了一坨东西。这坨东西很简单,就是下传标记。在经过 splay 操作后,当前树的父子关系发生改变,所以要在此之前,把「债」都还清了,才能双旋;
  3. 双旋的结束标准是有无转到当前 Splay 的根。

其他注意点#

  1. 关于 pushup,注意在任何一个父子关系改变的时候都应当思考是否需要 pushup
  2. 更改的时候一定一定要注意对别的节点有没有影响,大多数时候都是要 splay 之后再更改的;
  3. To be continued......

例题#

关于用 LCT 完成一些类似平衡树或者树剖的动态问题:通常需要考虑每个节点维护什么信息,是否足够维护最终答案,一般是维护一条原树上的链的信息,那么在 Splay 中就是子树信息了,比较好维护。

人生第一道 LCT 吧。。。基操,套板子就行了。

My Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
using namespace std;
const int MAXN=1e5+10;
struct LCT{
	#define ls tr[x].ch[0]
	#define rs tr[x].ch[1]
	struct Tree{int ch[2],val,fa,rev,xv;}tr[MAXN];
	void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
	bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
	void pushup(int x){tr[x].xv=tr[ls].xv^tr[rs].xv^tr[x].val;}
	void pushdown(int x){
		if(!tr[x].rev) return;
		if(ls)pushr(ls);if(rs)pushr(rs);tr[x].rev=0;
	}
	void rot(int x){
		int f=tr[x].fa,k=(x==tr[f].ch[1]),g=tr[f].fa,v=tr[x].ch[k^1];
		if(!isroot(f)) tr[g].ch[f==tr[g].ch[1]]=x;tr[x].ch[k^1]=f;tr[f].ch[k]=v;
		if(v) tr[v].fa=f;tr[f].fa=x;tr[x].fa=g;
		pushup(f);
	}int stk[MAXN],top;
	void splay(int x){
		int tmp=x;top=0;stk[++top]=tmp;
		while(!isroot(tmp)) tmp=tr[tmp].fa,stk[++top]=tmp;
		while(top) pushdown(stk[top--]);
		while(!isroot(x)){
			int f=tr[x].fa,g=tr[f].fa;
			if(!isroot(f))
				rot((f==tr[g].ch[1])^(x==tr[f].ch[1])?x:f);
			rot(x);
		}pushup(x);
	}
	void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}
	void makeroot(int x){access(x);splay(x);pushr(x);}
	int findroot(int x){access(x);splay(x);while(ls) pushdown(x),x=ls;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) tr[x].fa=y;}
	void cut(int x,int y){
		makeroot(x);
		if(findroot(y)!=x||tr[y].fa!=x||tr[y].ch[0]) return;
		tr[y].fa=tr[x].ch[1]=0;pushup(x);
	}
}T;
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&T.tr[i].val);
	int op,x,y;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&op,&x,&y);
		if(op==0) T.split(x,y),printf("%d\n",T.tr[y].xv);
		else if(op==1) T.link(x,y);
		else if(op==2) T.cut(x,y);
		else T.splay(x),T.tr[x].val=y;
	}
}

P3203 [HNOI2010]弹飞绵羊#

利用性质——每个位置在当前状况下有唯一后继,那我们把这个后继当成它的父亲,建一棵树,根为空,那么最终答案就是每个节点到根的距离。
如果和我一开始想得一样去维护深度显然是会 GG 的,于是考虑这个深度是什么。考虑对 x 进行一个 access(x),这样的话根就到 x 形成了一条链,答案就是根的 siz。然后记得 splay(x) 以保证复杂度。既然这样,不妨就输出 tr[x].siz 算了。

然后更改 gap 就直接 link-cut 就好了。但是我发现我逊了,FlashHu:

查询原本需要 split,我们直接 access(x),splay(x),输出 xsize
连边原本需要 link,题目保证了是一棵树,我们直接改 x 的父亲,连轻边。
断边原本需要 cut,然而我们确定其父亲的位置,access(x),splay(x) 后,x 的父亲一定在 x 的左子树中(LCT 总结中的性质 1),直接双向断开连接。

但是由于玄学原因,T 了好多发。x

My Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
using namespace std;
const int MAXN=2e5+10;
struct LCT{ 
	#define ls tr[x].ch[0]
	#define rs tr[x].ch[1]
	struct node{int ch[2],fa,siz;}tr[MAXN];
	bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
	void pushup(int x){tr[x].siz=tr[ls].siz+tr[rs].siz+1;}
	void rot(int x){
		int f=tr[x].fa,g=tr[f].fa,k=(x==tr[f].ch[1]),v=tr[x].ch[k^1];
		if(!isroot(f)) tr[g].ch[f==tr[g].ch[1]]=x;tr[x].fa=g;
		if(v) tr[v].fa=f;tr[f].ch[k]=v; tr[x].ch[k^1]=f,tr[f].fa=x;
		pushup(f);
	}
	void splay(int x){
		while(!isroot(x)){
			int f=tr[x].fa,g=tr[f].fa;
			if(!isroot(f))
				rot((tr[f].ch[1]==x)^(tr[g].ch[1]==f)?x:f);
			rot(x);
		}pushup(x);
	}
	void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}
}T;
int gp[MAXN];
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&gp[i]);
		if(i+gp[i]<=n) T.tr[i].fa=i+gp[i];
	}
	int Q,op,x,y;scanf("%d",&Q);
	while(Q--){
		scanf("%d",&op);
		if(op==1){
			scanf("%d",&x);x++;
			T.access(x);T.splay(x);
			printf("%d\n",T.tr[x].siz);
		}else{
			scanf("%d%d",&x,&y);x++;
			T.access(x);T.splay(x);
			T.tr[x].ch[0]=T.tr[T.ls].fa=0;
			gp[x]=y;if(x+gp[x]<=n) T.tr[x].fa=x+gp[x];
		}
	}
}

P1501 [国家集训队]Tree II#

比较直接的信息维护,和线段树 2 差不多,维护乘法标记和加法标记就可以了。注意由于其子树大小是不确定的,所以需要维护每棵 Splay 的大小。

My Code
#include<bits/stdc++.h>
#define int long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
using namespace std;
const int MAXN=1e5+10;
const int MOD=51061;
struct LCT{
	#define ls tr[x].ch[0]
	#define rs tr[x].ch[1]
	struct node{int ch[2],fa,rev,add,mul,sum,siz,val;}tr[MAXN];
	bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
	void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
	void pushup(int x){tr[x].sum=(tr[ls].sum+tr[rs].sum+tr[x].val)%MOD;tr[x].siz=tr[ls].siz+tr[rs].siz+1;}
	void Add(int x,int val){tr[x].sum=(tr[x].sum+val*tr[x].siz)%MOD;tr[x].add=(tr[x].add+val)%MOD;tr[x].val=(tr[x].val+val)%MOD;}
	void Mul(int x,int val){tr[x].sum=tr[x].sum*val%MOD;tr[x].add=tr[x].add*val%MOD;tr[x].mul=tr[x].mul*val%MOD;tr[x].val=tr[x].val*val%MOD;}
	void pushdown(int x){
		if(tr[x].rev){
			if(ls)pushr(ls);if(rs)pushr(rs);
			tr[x].rev=0;
		}if(tr[x].mul!=1){
			if(ls)Mul(ls,tr[x].mul);if(rs)Mul(rs,tr[x].mul);
			tr[x].mul=1;
		}if(tr[x].add){
			if(ls)Add(ls,tr[x].add);if(rs)Add(rs,tr[x].add);
			tr[x].add=0;
		}
	}
	void rot(int x){
		int f=tr[x].fa,g=tr[f].fa,k=(x==tr[f].ch[1]),w=tr[x].ch[k^1];
		if(!isroot(f)) tr[g].ch[f==tr[g].ch[1]]=x;tr[x].fa=g;
		if(w) tr[w].fa=f;tr[f].ch[k]=w;tr[x].ch[k^1]=f;tr[f].fa=x;
		pushup(f);
	}int stk[MAXN],top;
	void splay(int x){
		int tmp=x;top=0;stk[++top]=tmp;
		while(!isroot(tmp)) tmp=tr[tmp].fa,stk[++top]=tmp;
		while(top) pushdown(stk[top--]);
		while(!isroot(x)){
			int f=tr[x].fa,g=tr[f].fa;
			if(!isroot(f))
				rot((x==tr[f].ch[1])^(f==tr[g].ch[1])?x:f);
			rot(x);
		}pushup(x);
	}
	void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}
	void makeroot(int x){access(x);splay(x);pushr(x);}
	int findroot(int x){access(x);splay(x);while(ls)pushdown(x),x=ls;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)tr[x].fa=y;}
	void cut(int x,int y){
		makeroot(x);
		if(findroot(y)!=x||tr[y].fa!=x||tr[y].ch[0]) return;
		tr[x].ch[1]=tr[y].fa=0;
	}
}T;
signed main()
{
	int n,q;
	scanf("%lld%lld",&n,&q);
	for(int i=1;i<=n;i++) T.tr[i].mul=T.tr[i].val=1,T.tr[i].sum=T.tr[i].add=T.tr[i].rev=0;
	for(int i=2,u,v;i<=n;i++){
		scanf("%lld%lld",&u,&v);
		T.link(u,v);
	}
	char op;int a,b,c,d;
	while(q--){
		scanf(" %c",&op);
		if(op=='+'){
			scanf("%lld%lld%lld",&a,&b,&c);
			T.split(a,b);T.Add(b,c);
		}else if(op=='-'){
			scanf("%lld%lld%lld%lld",&a,&b,&c,&d);
			T.cut(a,b);T.link(c,d);
		}else if(op=='*'){
			scanf("%lld%lld%lld",&a,&b,&c);
			T.split(a,b);T.Mul(b,c);
		}else{
			scanf("%lld%lld",&a,&b);
			T.split(a,b);
			printf("%lld\n",T.tr[b].sum);
		}
	}
}

P4332 [SHOI2014]三叉神经树#

考虑每个节点的状态数:其实就是其儿子中 1 的个数,不妨定义:儿子中的 1 的数两为该节点的状态。那么容易得到,一个节点的状态为 0 或者 1 的时候会传递出 0 状态为 2 或者 3 的时候会传递出 1。这样我们就可以预处理出每个节点的状态。

接下来考虑修改。如果我们把某一个叶子从 0 变成 1,那么显然他父亲的状态数就 +1,此时如果父亲本来的状态是 0 或者 2(不可能是 3)是不会对父亲的祖先状态产生影响的。

所以,只要支持把从叶子开始向上连续的一段 1 全部变成 2 就可以了。把叶子的 1 变成 0 同理。

那么考虑每个节点维护什么东西。

每棵 Splay 中,我们想知道的是中序遍历最大的不是 1(或者 2)的节点,然后我们把这个节点旋到根,对右子树区间修改就可以了。那如果整棵 Splay 都是 1,那就全局修改,然后向上跳到上一棵 Splay 继续做同样的事情。

现在的问题就是维护每棵 Splay 中中序遍历最大的非 1/2 节点。我们用一个 id 来存它,每次合并 lr,然后如果右子树中有,那就用右子树的,否则用根的,如果根也不是就用左子树。

看了眼题解,不用在 Splay 之间跳来跳去,不然会 T,只需要 access 一下就好惹。这样就在一棵 Splay 中了。

My Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
using namespace std;
const int MAXN=2e6+10;
struct LCT{
    #define ls tr[x].ch[0]
    #define rs tr[x].ch[1]
    struct node{int ch[2],fa,id[3],val,tag,sum;}tr[MAXN];
    bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
    void don(int x,int v){tr[x].sum+=v;tr[x].val=tr[x].sum>1;swap(tr[x].id[1],tr[x].id[2]);tr[x].tag+=v;}
    void pushup(int x){
        for(int i=1;i<=2;i++){
            if(tr[rs].id[i]) tr[x].id[i]=tr[rs].id[i];
            else if(tr[x].sum!=i) tr[x].id[i]=x;
            else tr[x].id[i]=tr[ls].id[i];
        }
    }
    void pushdown(int x){
        if(!tr[x].tag) return;
        if(ls)don(ls,tr[x].tag);if(rs)don(rs,tr[x].tag);tr[x].tag=0;
    }
    void rot(int x){
		int f=tr[x].fa,k=(x==tr[f].ch[1]),g=tr[f].fa,v=tr[x].ch[k^1];
		if(!isroot(f)) tr[g].ch[f==tr[g].ch[1]]=x;tr[x].ch[k^1]=f;tr[f].ch[k]=v;
		if(v) tr[v].fa=f;tr[f].fa=x;tr[x].fa=g;
		pushup(f);
	}int stk[MAXN],top;
	void splay(int x){
		int tmp=x;top=0;stk[++top]=tmp;
		while(!isroot(tmp)) tmp=tr[tmp].fa,stk[++top]=tmp;
		while(top) pushdown(stk[top--]);
		while(!isroot(x)){
			int f=tr[x].fa,g=tr[f].fa;
			if(!isroot(f))
				rot((f==tr[g].ch[1])^(x==tr[f].ch[1])?x:f);
			rot(x);
		}pushup(x);
	}
    void access(int x){
        for(int s=0;x;s=x,x=tr[x].fa)
            splay(x),rs=s,pushup(x);
    }
}T;
vector<int> e[MAXN];int n;
void dfs(int x,int fa){
	T.tr[x].sum=0;
    for(int s:e[x]){
        if(s==fa) continue;
        dfs(s,x);T.tr[x].sum+=T.tr[s].val;
    }if(x<=n) T.tr[x].val=(T.tr[x].sum>1);
}
int main()
{
    scanf("%d",&n);
    for(int i=1,x1,x2,x3;i<=n;i++){
        scanf("%d%d%d",&x1,&x2,&x3);
        e[i].pb(x1);e[i].pb(x2);e[i].pb(x3);
        T.tr[x1].fa=i;T.tr[x2].fa=i;T.tr[x3].fa=i;
    }
    for(int i=n+1;i<=3*n+1;i++) scanf("%d",&T.tr[i].val);
    dfs(1,0);
    int Q,x,ans=T.tr[1].val;scanf("%d",&Q);
    while(Q--){
        scanf("%d",&x);int tmp=x;x=T.tr[x].fa;
        int tg=T.tr[tmp].val?-1:1,p;
        T.access(x);T.splay(x);
        if(T.tr[tmp].val) p=T.tr[x].id[2];
        else p=T.tr[x].id[1];
        if(p){
            T.splay(p);
            T.don(T.tr[p].ch[1],tg);T.pushup(T.tr[p].ch[1]);
            T.tr[p].sum+=tg;T.tr[p].val=(T.tr[p].sum>1);T.pushup(p);
        }else ans^=1,T.don(x,tg),T.pushup(x);
        T.tr[tmp].val^=1;
        printf("%d\n",ans);
    }
}

关于维护图的连通性问题,不需要维护什么,但是有的时候需要维护点双或者边双,那么需要一些辅助的信息。不会太难。

P2147 [SDOI2008] 洞穴勘测#

要求支持带撤销的并查集。很快想到用 LCT 维护。但是有一个小问题就是原树一定是棵树,那如果多余的连边怎么办。

哦那没事了,题目保证不出现环。

那不就是裸题了么(((

My Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define bp push_back
#define pii pair<int,int>
#define mkp make_pair
#define fi first
#define se second
using namespace std;
const int MAXN=1e4+10;
struct node{int ch[2],fa,rev;}tr[MAXN];
#define ls tr[x].ch[0]
#define rs tr[x].ch[1]
bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
void pushdown(int x){if(!tr[x].rev)return;if(ls)pushr(ls);if(rs)pushr(rs);tr[x].rev=0;}
void rot(int x){
	int f=tr[x].fa,g=tr[f].fa,k=(x==tr[f].ch[1]),v=tr[x].ch[k^1];
	if(!isroot(f)) tr[g].ch[tr[g].ch[1]==f]=x;tr[x].fa=g;tr[f].ch[k]=v;
	if(v) tr[v].fa=f;tr[f].fa=x;tr[x].ch[k^1]=f;
}int stk[MAXN],top;
void splay(int x){
	int tmp=x;top=0;stk[++top]=x;
	while(!isroot(tmp))tmp=tr[tmp].fa,stk[++top]=tmp;
	while(top)pushdown(stk[top--]);
	while(!isroot(x)){
		int f=tr[x].fa,g=tr[f].fa;
		if(!isroot(f)) rot((tr[f].ch[1]==x)^(tr[g].ch[1]==f)?x:f);
		rot(x);
	}
}
void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s;}
void makeroot(int x){access(x);splay(x);pushr(x);}
int findroot(int x){access(x);splay(x);while(ls)pushdown(x),x=ls;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)tr[x].fa=y;}
void cut(int x,int y){split(x,y);tr[x].fa=tr[y].ch[0]=0;}
int main()
{
	int n,m,x,y;scanf("%d%d",&n,&m);
	char s[11];
	while(m--){
		scanf("%s%d%d",s,&x,&y);
		if(s[0]=='C') link(x,y);
		else if(s[0]=='D') cut(x,y);
		else puts(findroot(x)==findroot(y)?"Yes":"No");
	}
}

P2542 [AHOI2005] 航线规划#

LCT 维护边双联通分量,用树剖也能做,因为题目中保证了图的连通性。

考虑删边非常困难,于是反过来考虑倒序加边。在加边的过程中,只会使桥变为非桥,因此我们边转点后,每个点记录一个东西表示这个点代表的边是不是桥。然后我们去连边,每次把端点之间的边全部赋值为 0,因为这些边都不能是桥了。然后每次查询路径上权值和就行了。

其实用树剖常数会小很多但是 LCT 少一只 log但是毕竟我们要练习 LCT 嘛。

My Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
#define pii pair<int,int>
#define mkp make_pair
#define fi first
#define se second
using namespace std;
const int MAXN=2e5+10;
struct node{int ch[2],fa,rev,tag,num,brg;}tr[MAXN];
#define ls tr[x].ch[0]
#define rs tr[x].ch[1]
bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
void dec(int x){tr[x].brg=0;tr[x].num=0;tr[x].tag=-1;}
void pushup(int x){tr[x].num=tr[ls].num+tr[rs].num+tr[x].brg;}
void pushdown(int x){
	if(tr[x].rev){if(ls)pushr(ls);if(rs)pushr(rs);tr[x].rev=0;}
	if(tr[x].tag==-1){if(ls)dec(ls);if(rs)dec(rs);tr[x].tag=0;}
}
void rot(int x){
	int f=tr[x].fa,g=tr[f].fa,k=(x==tr[f].ch[1]),v=tr[x].ch[k^1];
	if(!isroot(f)) tr[g].ch[tr[g].ch[1]==f]=x;tr[x].fa=g;tr[f].ch[k]=v;
	if(v) tr[v].fa=f;tr[x].ch[k^1]=f;tr[f].fa=x;pushup(f);
}
int stk[MAXN],top;
void splay(int x){
	int tmp=x;top=0;stk[++top]=x;
	while(!isroot(tmp))tmp=tr[tmp].fa,stk[++top]=tmp;
	while(top)pushdown(stk[top--]);
	while(!isroot(x)){
		int f=tr[x].fa,g=tr[f].fa;
		if(!isroot(f)) rot((tr[f].ch[1]==x)^(tr[g].ch[1]==f)?x:f);
		rot(x);
	}pushup(x);
}
void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}
void makeroot(int x){access(x);splay(x);pushr(x);}
int findroot(int x){access(x);splay(x);while(ls)pushdown(x),x=ls;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)tr[x].fa=y;}
bool check(int x,int y){makeroot(x);return findroot(y)!=x;}
int n;
int G(int x,int y){return x*n+y;}
map<int,int> vis,id;
struct Query{int op,u,v;}q[MAXN];
vector<pii> E;
int main()
{
	int m;
	scanf("%d%d",&n,&m);
	for(int i=1,u,v;i<=m;i++){
		scanf("%d%d",&u,&v);
		E.pb(mkp(u,v));tr[n+i].brg=1;
		id[G(u,v)]=id[G(v,u)]=i;
	}
	int tot=1;
	while(~scanf("%d",&q[tot].op)&&q[tot].op!=-1){
		scanf("%d%d",&q[tot].u,&q[tot].v);
		if(q[tot].op!=1) vis[G(q[tot].u,q[tot].v)]=vis[(G(q[tot].v,q[tot].u))]=1;
		tot++;
	}tot--;
	for(auto s:E){
		if(vis[G(s.fi,s.se)]) continue;
		if(!check(s.fi,s.se)){
			split(s.fi,s.se);
			dec(s.se);
		}else{
			link(s.fi,n+id[G(s.fi,s.se)]);
			link(n+id[G(s.fi,s.se)],s.se);
		}
	}
	vector<int> ans;
	for(int i=tot;i>=1;i--){
		if(q[i].op==1){
			split(q[i].u,q[i].v);
			ans.pb(tr[q[i].v].num);
			continue;
		}
		if(!check(q[i].u,q[i].v)){
			split(q[i].u,q[i].v);
			dec(q[i].v);
		}else{
			link(q[i].u,n+id[G(q[i].u,q[i].v)]);
			link(n+id[G(q[i].u,q[i].v)],q[i].v);
		}
	}for(int i=(int)ans.size()-1;i>=0;i--) printf("%d\n",ans[i]);
}

关于用 LCT 维护边权(最小生成树)

题目做多了就会知道,如果要用 LCT 维护一条路径上边权的信息,由于是旋转平衡树,无法很好地表示。所以我们一般会建 n+m 个点分别表示点和边。

P4172 [WC2006]水管局长#

大概就是动态维护一个最小生成树。考虑到没有 link……实际上不妨时间倒流变成没有 cut 比较舒服。然后倒着加边,维护链上最大值就好了。

搞了半天 LCT 是没法很好地维护边权的,所以 LCT 在处理这类题目的时候需要加入 n+m 个点/qd。

My Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
#define pii pair<int,int>
#define mkp make_pair
#define fi first
#define se second
#define rep(i,j,k) for(int i=(j);i<=(k);i++)
#define per(i,j,k) for(int i=(j);i>=(k);i--)
#define pt(a) cerr<<#a<<'='<<a<<' '
#define pts(a) cerr<<#a<<'='<<a<<'\n'
using namespace std;
const int MAXN=1e3+10;
const int MAXM=2e5+10;
struct Edge{int u,v,w;}e[MAXM];
int eid[MAXN][MAXN];
struct Tree{int ch[2],fa,rev,mx,id;}tr[MAXM];
#define ls tr[x].ch[0]
#define rs tr[x].ch[1]
bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
int kd(int x){return tr[tr[x].fa].ch[1]==x;}
void pushup(int x){
	tr[x].mx=tr[x].id;
	if(e[tr[ls].mx].w>e[tr[x].mx].w) tr[x].mx=tr[ls].mx;
	if(e[tr[rs].mx].w>e[tr[x].mx].w) tr[x].mx=tr[rs].mx;
}
void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
void pushdown(int x){
	if(!tr[x].rev) return;
	if(ls)pushr(ls);
	if(rs)pushr(rs);
	tr[x].rev=0;
}
void rot(int x){
	int f=tr[x].fa,g=tr[f].fa,d=kd(x),v=tr[x].ch[d^1];
	if(!isroot(f)) tr[g].ch[kd(f)]=x;tr[x].fa=g;tr[f].ch[d]=v;
	if(v) tr[v].fa=f;tr[x].ch[d^1]=f;tr[f].fa=x;pushup(f);
}
int stk[MAXN],top;
void splay(int x){
	top=0;int tmp=x;stk[++top]=tmp;
	while(!isroot(tmp)) tmp=tr[tmp].fa,stk[++top]=tmp;
	while(top) pushdown(stk[top--]);
	while(!isroot(x)){
		int f=tr[x].fa;
		if(!isroot(f))
			rot(kd(x)^kd(f)?x:f);
		rot(x);
	}pushup(x);
}
void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}
void makeroot(int x){access(x);splay(x);pushr(x);}
int findroot(int x){access(x);splay(x);while(ls) pushdown(x),x=ls;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) tr[x].fa=y;}
void cut(int x,int y){split(x,y);tr[x].fa=tr[y].ch[0]=0;}
bool ban[MAXN][MAXN];
struct Query{
	int k,u,v;void input(){
		cin>>k>>u>>v;
		if(k==2) ban[u][v]=ban[v][u]=1;
	}
}q[MAXM];
int f[MAXN];
int find(int x){while(f[x]^x)x=f[x]=f[f[x]];return x;}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n,m,Q;cin>>n>>m>>Q;
	rep(i,1,m) cin>>e[i].u>>e[i].v>>e[i].w;
	sort(e+1,e+1+m,[&](Edge a,Edge b){return a.w<b.w;});
	rep(i,1,m) eid[e[i].u][e[i].v]=eid[e[i].v][e[i].u]=i;
	rep(i,1,Q) q[i].input();
	reverse(q+1,q+1+Q);
	rep(i,1,n) f[i]=i;
	rep(i,1,n+m) tr[i].id=tr[i].mx=(i<=n?0:i-n);
	int cnt=0;
	rep(i,1,m){
		int u=e[i].u,v=e[i].v;
		if(ban[u][v]||find(u)==find(v)) continue;
		f[find(u)]=find(v);link(u,n+i);link(n+i,v);
		if(++cnt==n-1) break;
	}
	vector<int> ans;
	rep(i,1,Q){
		if(q[i].k==1){
			split(q[i].u,q[i].v);
			ans.pb(e[tr[q[i].v].mx].w);
		}else{
			split(q[i].u,q[i].v);
			int mid=tr[q[i].v].mx;
			int qid=eid[q[i].u][q[i].v];
			if(e[mid].w<=e[qid].w) continue;
			cut(e[mid].u,n+mid);cut(n+mid,e[mid].v);
			link(e[qid].u,qid+n);
			link(qid+n,e[qid].v);
		}
	}
	reverse(ans.begin(),ans.end());
	for(int s:ans) cout<<s<<'\n';
	return 0;
}

P4234 最小差值生成树#

标题即题意。乍一看题——这和 LCT 有什么关系?对这题进行一个模糊理解,就是最小差值,它的边一定是在边权排序后的一段连续区间内取的。于是我们容易想到滑动窗口来更新答案。然而此时会出现一个问题,就是在左端点加一的时候,可能右端点不必要加一,而是把之前没有加入的边加入进来。此时显然不能回过头去考虑这个东西,所以我们大概是需要一个更优美的做法。

FlashHu 给出的做法是:按序加入边,然后如果已经构成树就替换然后更新答案,否则直接连。我们尝试证明这个东西。首先在选定初始的边的时候,由于选了最小的边,所以最大的边越小越好。然后考虑加入一条新的边,此时我们容易发现,我们希望剩下的边中最小值最大,那我们所能做的就是选择把环上的哪条边断掉,为了最小值最大,那就是环上的最小边。

My Code
#include<bits/stdc++.h>
#define int long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
#define pii pair<int,int>
#define mkp make_pair
#define fi first
#define se second
#define rep(i,j,k) for(int i=(j);i<=(k);i++)
#define per(i,j,k) for(int i=(j);i>=(k);i--)
#define pt(a) cerr<<#a<<'='<<a<<' '
#define pts(a) cerr<<#a<<'='<<a<<'\n'
using namespace std;
const int MAXN=5e4+10;
const int MAXM=2e5+10;
struct Edge{int u,v,w;}e[MAXM];
struct Tree{int ch[2],fa,rev,id,mn;}tr[MAXN+MAXM];
#define ls tr[x].ch[0]
#define rs tr[x].ch[1]
bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
int kd(int x){return tr[tr[x].fa].ch[1]==x;}
void pushup(int x){
	tr[x].mn=tr[x].id;
	if(e[tr[x].mn].w>e[tr[ls].mn].w) tr[x].mn=tr[ls].mn;
	if(e[tr[x].mn].w>e[tr[rs].mn].w) tr[x].mn=tr[rs].mn;
}
void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
void pushdown(int x){
	if(!tr[x].rev) return;
	if(ls) pushr(ls);
	if(rs) pushr(rs);
	tr[x].rev=0;
}
void rot(int x){
	int f=tr[x].fa,g=tr[f].fa,d=kd(x),v=tr[x].ch[d^1];
	if(!isroot(f)) tr[g].ch[kd(f)]=x;tr[x].fa=g;tr[f].ch[d]=v;
	if(v) tr[v].fa=f;tr[x].ch[d^1]=f;tr[f].fa=x;pushup(f);
}
int stk[MAXN],top;
void splay(int x){
	top=0;int tmp=x;stk[++top]=tmp;
	while(!isroot(tmp)) tmp=tr[tmp].fa,stk[++top]=tmp;
	while(top) pushdown(stk[top--]);
	while(!isroot(x)){
		int f=tr[x].fa;
		if(!isroot(f))
			rot(kd(x)^kd(f)?x:f);
		rot(x);
	}pushup(x);
}
void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}
void makeroot(int x){access(x);splay(x);pushr(x);}
int findroot(int x){access(x);splay(x);while(ls) pushdown(x),x=ls;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) tr[x].fa=y;}
void cut(int x,int y){split(x,y);tr[x].fa=tr[y].ch[0]=0;}
int f[MAXN];
int find(int x){while(f[x]^x)x=f[x]=f[f[x]];return x;}
int vis[MAXM];
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n,m;cin>>n>>m;
	rep(i,1,m) cin>>e[i].u>>e[i].v>>e[i].w;
	sort(e+1,e+1+m,[&](Edge a,Edge b){return a.w<b.w;});
	rep(i,1,n) f[i]=i;
	e[0].w=inf;
	rep(i,1,n+m) tr[i].id=tr[i].mn=(i>n?i-n:0);
	int cnt=0,ans=inf,lt=1;
	rep(i,1,m){
		int u=e[i].u,v=e[i].v;
		if(find(u)!=find(v)){
			f[find(u)]=find(v);
			link(u,n+i);link(n+i,v);
			vis[i]=1;
			while(!vis[lt]) lt++;
			if(++cnt==n-1) ans=min(ans,e[i].w-e[lt].w);
		}else{
			if(u==v) continue;
			split(u,v);
			int mid=tr[v].mn;
			cut(e[mid].u,mid+n);cut(mid+n,e[mid].v);
			link(u,n+i);link(n+i,v);
			vis[i]=1;vis[mid]=0;
			while(!vis[lt]) lt++;
			if(cnt==n-1)ans=min(ans,e[i].w-e[lt].w);
		}
	}cout<<ans<<'\n';
	return 0;
}

P2387 [NOI2014] 魔法森林#

题意就是要求对于两种不同的权值求一棵最小生成树使得 1n 的路径上第一种权值的最大值加上第二种权值的最大值最小。

看到两个关键字肯定是不好维护的,然后我们发现这题其实和上面那题有那么一点像,于是容易想到按第一关键字把边排个序。然后呢在确定了第一关键字的范围之后,我们就可以维护第二关键字的最小生成树。然后我们像上一题那样,从小到大强制某一条边必选,然后同时维护第二关键字的最小生成树。

还是不太会,看了题解。排序然后维护 MST 是对的。然后主要是维护的时候还要考虑第一关键字的问题。如果我们当前掏出一条边,它的第二权值比环上的最大边权还大,那么它就被二维偏序了,肯定不用它。反之,我们考虑如果当前它可以联通两个联通块,那么它肯定是比后续的边更优的。所以直接连就可以了。

My Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
#define pii pair<int,int>
#define mkp make_pair
#define fi first
#define se second
#define rep(i,j,k) for(int i=(j);i<=(k);i++)
#define per(i,j,k) for(int i=(j);i>=(k);i--)
#define pt(a) cerr<<#a<<'='<<a<<' '
#define pts(a) cerr<<#a<<'='<<a<<'\n'
using namespace std;
const int MAXN=2e5+10;
struct Edge{int u,v,a,b;}e[MAXN];
struct Tree{int ch[2],fa,rev,id,mx;}tr[MAXN];
#define ls tr[x].ch[0]
#define rs tr[x].ch[1]
bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
int kd(int x){return tr[tr[x].fa].ch[1]==x;}
void pushup(int x){
	tr[x].mx=tr[x].id;
	if(e[tr[ls].mx].b>e[tr[x].mx].b) tr[x].mx=tr[ls].mx;
	if(e[tr[rs].mx].b>e[tr[x].mx].b) tr[x].mx=tr[rs].mx;
}
void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
void pushdown(int x){
	if(!tr[x].rev) return;
	if(ls) pushr(ls);
	if(rs) pushr(rs);
	tr[x].rev=0;
}
void rot(int x){
	int f=tr[x].fa,g=tr[f].fa,d=kd(x),v=tr[x].ch[d^1];
	if(!isroot(f)) tr[g].ch[kd(f)]=x;tr[x].fa=g;tr[f].ch[d]=v;
	if(v) tr[v].fa=f;tr[x].ch[d^1]=f;tr[f].fa=x;pushup(f);
}
int stk[MAXN],top;
void splay(int x){
	top=0;int tmp=x;stk[++top]=tmp;
	while(!isroot(tmp)) tmp=tr[tmp].fa,stk[++top]=tmp;
	while(top) pushdown(stk[top--]);
	while(!isroot(x)){
		int f=tr[x].fa;
		if(!isroot(f))
			rot(kd(x)^kd(f)?x:f);
		rot(x);
	}pushup(x);
}
void access(int x){for(int s=0;x;s=x,x=tr[x].fa)splay(x),rs=s,pushup(x);}
void makeroot(int x){access(x);splay(x);pushr(x);}
int findroot(int x){access(x);splay(x);while(ls)pushdown(x),x=ls;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) tr[x].fa=y;}
void cut(int x,int y){split(x,y);tr[x].fa=tr[y].ch[0]=0;}
int f[MAXN];
int find(int x){while(x^f[x]) x=f[x]=f[f[x]];return x;}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n,m;cin>>n>>m;
	rep(i,1,m) cin>>e[i].u>>e[i].v>>e[i].a>>e[i].b;
	sort(e+1,e+1+m,[&](Edge x,Edge y){return x.a<y.a;});
	rep(i,1,n+m) tr[i].id=tr[i].mx=(i>n?i-n:0);
	rep(i,1,n) f[i]=i;
	int ans=inf;
	rep(i,1,m){
		int u=e[i].u,v=e[i].v;
		if(find(u)!=find(v)){
			f[find(u)]=find(v);
			link(u,n+i);link(n+i,v);
		}else{
			split(u,v);
			int mid=tr[v].mx;
			if(e[i].b<e[mid].b){
				cut(e[mid].u,n+mid);
				cut(n+mid,e[mid].v);
				link(u,n+i);link(n+i,v);
			}
		}if(find(1)==find(n))
			split(1,n),ans=min(ans,e[tr[n].mx].b+e[i].a);
	}if(ans==inf) ans=-1;
	cout<<ans<<'\n';
	return 0;
}

关于用 LCT 维护子树的信息。我们知道,LCT 是容易通过 access(x) 从而维护一条链上的信息,但是在遇到子树的问题的时候好像并不好维护。所以之后凡是遇到子树的问题还是尽量用树剖或者 dfn 展开。

但是 LCT 好啊,可以瞎几把断边连边。巴适~一旦遇到需要维护子树并且需要断边连边的题,树剖就变得束手无策了。这时候就需要 LCT 大展身手。我们通过一些魔改使得 LCT 能够维护子树信息。

首先,我们考虑我们其实已经知道了子树中的一部分信息——实链上的信息。所以我们的问题就是——如何得到剩下的信息。其实也很简单,我们定义一个 six 表示与 x 节点用虚边相连的子树的信息和,以及 sx 表示 x 子树的信息和。这时候,我们假设已经维护好了 six,那 pushup(x) 就可以这么写:

void pushup(int x){tr[x].s=tr[ls].s+tr[rs].s+tr[x].is+tr[x].val;}

很好理解,接下里主要考虑怎么维护 six。我们对 LCT 的操作逐个分析。

Access 有虚边边实边的操作,我们在变的时候改信息就行了:

void access(int x){
    for(int s=0;x;s=x,x=tr[x].fa){
        splay(x);tr[x].si+=tr[rs].s;
        tr[x].si-=tr[rs=s].s;pushup(x);
    }
}

Makeroot 没有影响
Findroot 没有影响
Split 没有影响
Link 注意由于连了一个新的子树,所以需要改一下:

void link(int x,int y){
    makeroot(x); makeroot(y);
    tr[tr[x].fa=y].si+=tr[x].s;
    pushup(y);
}

Cut 没有太大的影响,我们断的是实边,不会影响虚值。需要在搞完之后重新 pushup(x) 一下。

void cut(int x,int y){
	split(x,y);tr[x].fa=tr[y].ch[0]=0;
	pushup(x);
}

接下来分析一下用 LCT 维护子树信息的局限性,其中最重要的一点就是信息需要可减,比如最大值就很难维护了。当然,如果没有可减性,可以对每个节点开一个 DS 维护虚子树中的最大值(这样这常数就不是一般的大了)。

P4219 [BJOI2014]大融合#

维护子树大小,然后每次询问 x,y,先切开变成两棵树,然后分别 makeroot(x) 把大小乘起来,然后再 link 回去就好了。

My Code
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb push_back
#define pii pair<int,int>
#define mkp make_pair
#define fi first
#define se second
#define rep(i,j,k) for(int i=(j);i<=(k);i++)
#define per(i,j,k) for(int i=(j);i>=(k);i--)
#define pt(a) cerr<<#a<<'='<<a<<' '
#define pts(a) cerr<<#a<<'='<<a<<'\n'
using namespace std;
const int MAXN=1e5+10;
struct Tree{int ch[2],fa,rev,s,si;}tr[MAXN];
#define ls tr[x].ch[0]
#define rs tr[x].ch[1]
bool isroot(int x){return tr[tr[x].fa].ch[0]!=x&&tr[tr[x].fa].ch[1]!=x;}
void pushr(int x){swap(ls,rs);tr[x].rev^=1;}
void pushup(int x){tr[x].s=tr[ls].s+tr[rs].s+tr[x].si+1;}
int kd(int x){return tr[tr[x].fa].ch[1]==x;}
void pushdown(int x){
	if(!tr[x].rev) return;
	if(ls) pushr(ls);
	if(rs) pushr(rs);
	tr[x].rev=0;
}
void rot(int x){
	int f=tr[x].fa,g=tr[f].fa,d=kd(x),v=tr[x].ch[d^1];
	if(!isroot(f)) tr[g].ch[kd(f)]=x;tr[x].fa=g;tr[f].ch[d]=v;
	if(v) tr[v].fa=f;tr[f].fa=x;tr[x].ch[d^1]=f;pushup(f);
}
int stk[MAXN],top;
void splay(int x){
	top=0;int tmp=x;stk[++top]=tmp;
	while(!isroot(tmp)) tmp=tr[tmp].fa,stk[++top]=tmp;
	while(top) pushdown(stk[top--]);
	while(!isroot(x)){
		int f=tr[x].fa;
		if(!isroot(f))
			rot(kd(x)^kd(f)?x:f);
		rot(x);
	}pushup(x);
}
void access(int x){
	for(int s=0;x;s=x,x=tr[x].fa){
		splay(x);tr[x].si+=tr[rs].s;
		tr[x].si-=tr[rs=s].s;pushup(x);
	}
}
void makeroot(int x){access(x);splay(x);pushr(x);}
int findroot(int x){access(x);splay(x);while(ls) pushdown(x),x=ls;splay(x);return x;}
void split(int x,int y){makeroot(x);access(y);splay(y);}
void link(int x,int y){
	makeroot(x); makeroot(y);
	tr[tr[x].fa=y].si+=tr[x].s;
	pushup(y);
}
void cut(int x,int y){
	split(x,y);tr[x].fa=tr[y].ch[0]=0;
	pushup(x);
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n,Q;
	cin>>n>>Q;
	char op;int x,y;
	while(Q--){
		cin>>op>>x>>y;
		if(op=='A'){
			link(x,y);
		}else{
			cut(x,y);
			makeroot(x);makeroot(y);
			cout<<tr[x].s*tr[y].s<<'\n';
			link(x,y);
		}
	}
	return 0;
}
posted @   ZCETHAN  阅读(72)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
主题色彩