学习笔记——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 辅助树,使得其拥有一些性质:
- 每个 Splay 中维护的树中序遍历在原树中是一条链,深度是递增的。因此 Splay 中的前驱与后继就是原树中的父亲和儿子。
- 每个节点被包含且仅包含在一个 Splay 中。
- 各个 Splay 之间用虚边相连。所谓虚边,实际上就是从儿子能单向地找到父亲但是父亲却找不到儿子,因为父亲只认经过实边的那个唯一的儿子。
注意,以下所有操作都是针对与原树的,辅助树只是用来辅助的。
Access
LCT 的基本操作只有一个就是 access
。access(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);}
Link
从 \(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)
。
于是我们有一些判断来除去这些情况:
- 如果 \(x,y\) 不在同一个连通块中;
- 如果 \(y\) 的父亲不是 \(x\),那说明他们之间还有其它点;
- 如果 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);
}
这里包含了挺多东西的,但是 pushup
和 pushdown
大可不用管它。上面不同之处我都加了 //diff
。接下来我逐一解释:
- 单旋的时候,由于我们不能让 Splay 的根的父亲来认这个虚边连着的儿子,所以我们在 rot 的时候要注意其父亲是不是根;
- 发现在 \(splay\) 操作主体前多了一坨东西。这坨东西很简单,就是下传标记。在经过 \(splay\) 操作后,当前树的父子关系发生改变,所以要在此之前,把「债」都还清了,才能双旋;
- 双旋的结束标准是有无转到当前 Splay 的根。
其他注意点
- 关于 pushup,注意在任何一个父子关系改变的时候都应当思考是否需要 \(pushup\);
- 更改的时候一定一定要注意对别的节点有没有影响,大多数时候都是要 \(splay\) 之后再更改的;
- To be continued......
例题
关于用 LCT 完成一些类似平衡树或者树剖的动态问题:通常需要考虑每个节点维护什么信息,是否足够维护最终答案,一般是维护一条原树上的链的信息,那么在 Splay 中就是子树信息了,比较好维护。
P3690 【模板】动态树(Link Cut Tree)
人生第一道 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;
}