Loading

学习笔记——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\) 变成根,然后把 \(y\)\(x\) 的路径打通,再把 \(y\) 通过 splay(y) 上来。此时,\(y\) 是这个 Splay 的根,这条链的信息是存在 \(y\) 中的

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

\(x\)\(y\) 连一条边。先让 \(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) 了,此时如果 \(y\)\(x\) 直接相连,那么 \(y\) 必然是 \(x\) 的亲儿子中的一个。而在 findroot(y) 的时候,我们进行了 access(y),也就是 \(y\)\(x\) 在一棵 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)\),输出 \(x\)\(size\)
连边原本需要 \(link\),题目保证了是一棵树,我们直接改 \(x\) 的父亲,连轻边。
断边原本需要 \(cut\),然而我们确定其父亲的位置,\(access(x),splay(x)\) 后,\(x\) 的父亲一定在 \(x\) 的左子树中(LCT 总结中的性质 1),直接双向断开连接。

但是由于玄学原因,T 了好多发。\(\color{white}{主要是因为我以为 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 来存它,每次合并 \(l\)\(r\),然后如果右子树中有,那就用右子树的,否则用根的,如果根也不是就用左子树。

看了眼题解,不用在 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] 魔法森林

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

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

还是不太会,看了题解。排序然后维护 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 能够维护子树信息。

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

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

很好理解,接下里主要考虑怎么维护 \(si_x\)。我们对 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 @ 2022-01-25 19:34  ZCETHAN  阅读(66)  评论(0编辑  收藏  举报