蒟蒻的LCT入门

什么是LCT:度娘的解释

引子

很久很久以前,boshi创造了天地,也创造了众生。这个世界上有大约 3 ∗ 1 0 5 3*10^5 3105个可爱的生灵,boshi将他们命名为节点
我们的主角是一个叫做i的小节点。
神明boshi每天要处理许多的事物,于是就需要节点们的帮助。为了高效地完成这些事物,boshi告诉小节点们,他们每个人都需要加入两个组织:需要维护的树 T 1 i T1_i T1i一棵splay T 2 i T2_i T2i

组织的诞生

需要维护的树,这个一号组织,成员之间会构成一个联络网。但是联络网上有一些电话线是不能用的,这样的电话线叫做虚边,平时我们可以当它们不存在,但有时可以被激活。而可以使用的电话线叫做实边,是随时可用的电话线。
splay,这个二号组织,则可以看成一号组织旗下的一个小组织,能够用实边互相联络的铁哥们组成了这个组织。splay组织的等级制度森严,在1号组织中,地位越高(越靠近根节点)的节点,排在左边,否则排在右边。

access

有一天,小节点i忽然收到了一条紧急情报,要快速发给它所在1号组织的老大(根节点)。它要怎么做呢?
第一步,小节点i先成为splay组织的老大
第二步,小节点i在splay中右边替换为上一个传递情报的节点j,相当于挪用i到其原来splay中右边节点那条电话线去联络j。
第三步,信息传递到了小节点i的父亲手中,重复执行第一步
代码实现如下:(f是1号组织中的父亲,son是2号组织中的儿子)

void acc(int x) {int y=0;while(x) splay(x),son[x][1]=y,up(x),y=x,x=f[x];}

evert

神明boshi认为小节点i能力出众,决定让他成为1号组织老大。
节点i非常高兴,不过权力交接是要走仪式的。
于是它先通过access操作联系到了当前的老大,通知他此事。然后成为其所在splay的老大,这个时候,由于i成为了老大,所以i旗下2号组织的所有节点,根据与i的亲疏关系,地位也要变化——要整体交换!
打上翻转标记即可。

void evert(int x) {acc(x),splay(x),rev[x]^=1;}

split

x想要联络y,并想知道联络路径上有哪些节点。
于是节点y夺权成为了老大,x联络上y说明了自己的意愿,x成为组织2老大。这样在组织2中,y一定在x的左边,并且没有左子树。所以y旗下就是x到y的联络路径上的所有节点了。

void split(int x,int y) {evert(y),acc(x),splay(x);}

link

有一个节点y想要加入组织1,小节点x很同意。于是x先夺权成为老大,然后将老大之位禅让给了y。

void link(int x,int y) {evert(x),f[x]=y;}

cut

x和y闹矛盾了,决定带着各自的部下将组织1一分为二。
如果x和y有连边,那么split之后,x在组织2中的右儿子就是y,直接断裂它们的联系即可。

void cut(int x,int y) {split(x,y);if(son[x][0]==y) son[x][0]=f[y]=0;}

练习题

洛谷P3690

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int read() {
    char ch=' ';int q=0;
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
const int N=300005;
int n,m,top;
int son[N][2],f[N],rev[N],v[N],sum[N],s[N];//f:真实父亲
int isroot(int x) {return son[f[x]][0]!=x&&son[f[x]][1]!=x;}
int is(int x) {return son[f[x]][1]==x;}
void pd(int x) {
    if(!rev[x]) return;
    rev[son[x][0]]^=1,rev[son[x][1]]^=1,swap(son[x][0],son[x][1]);
    rev[x]=0;
}
void up(int x) {sum[x]=sum[son[x][0]]^sum[son[x][1]]^v[x];}
void spin(int x) {
    int fa=f[x],g=f[fa],t=is(x);
    if(!isroot(fa)) son[g][is(fa)]=x;
    f[fa]=x,f[x]=g,f[son[x][t^1]]=fa;
    son[fa][t]=son[x][t^1],son[x][t^1]=fa;
    up(fa),up(x);
}
void splay(int x) {
    s[++top]=x;for(int i=x;!isroot(i);i=f[i]) s[++top]=f[i];
    while(top) pd(s[top--]);
    while(!isroot(x)) {
        if(!isroot(f[x])) {
            if(is(x)^is(f[x])) spin(x);
            else spin(f[x]);
        }
        spin(x);
    }
}
void acc(int x) {int y=0;while(x) splay(x),son[x][1]=y,up(x),y=x,x=f[x];}
void evert(int x) {acc(x),splay(x),rev[x]^=1;}
void link(int x,int y) {evert(x),f[x]=y;}
void split(int x,int y) {evert(y),acc(x),splay(x);}
void cut(int x,int y) {split(x,y);if(son[x][0]==y) son[x][0]=f[y]=0;}
int main()
{
    int bj,x,y;
    n=read(),m=read();
    for(int i=1;i<=n;++i) v[i]=sum[i]=read();
    while(m--) {
        bj=read(),x=read(),y=read();
        if(bj==0) split(x,y),printf("%d\n",sum[x]);
        else if(bj==1) link(x,y);
        else if(bj==2) cut(x,y);
        else acc(x),splay(x),v[x]=y,up(x);
    }
    return 0;
}

bzoj3669
此题是按照a值排序后,依次加边,并维护最小生成树。并对每条边建立一个点来储存边权。
维护方式是如果边的u和v连通,则找到它们路径上b值最大的边,如果大于当前边,则删去那条边,加上当前边。
所以我们要在splay中维护最大b值节点编号。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
const int N=150005,M=100005,inf=0x3f3f3f3f;
int n,m,ans=inf,top;
struct node{int x,y,a,b;}e[M];
int f[N],son[N][2],rev[N],v[N],p[N],mx[N],s[N];
bool cmp(node xx,node yy) {return xx.a<yy.a;}
int find(int x) {
	if(p[x]==x) return x;
	p[x]=find(p[x]);return p[x];
}
void up(int x) {//mx:最大点权的点
	if(!x) return;
	mx[x]=x;
	if(son[x][0]&&v[mx[son[x][0]]]>v[mx[x]]) mx[x]=mx[son[x][0]];
	if(son[x][1]&&v[mx[son[x][1]]]>v[mx[x]]) mx[x]=mx[son[x][1]];
}
void pd(int x) {
	if(!rev[x]) return;
	if(son[x][0]) rev[son[x][0]]^=1;
	if(son[x][1]) rev[son[x][1]]^=1;
	rev[x]=0,swap(son[x][0],son[x][1]);
}
int is(int x) {return son[f[x]][1]==x;}
int isroot(int x) {return son[f[x]][0]!=x&&son[f[x]][1]!=x;}
void spin(int x) {
	int fa=f[x],g=f[fa],t=is(x);
	if(!isroot(fa)) son[g][is(fa)]=x;
	f[fa]=x,f[x]=g,f[son[x][t^1]]=fa;
	son[fa][t]=son[x][t^1],son[x][t^1]=fa;
	up(fa),up(x);
}
void splay(int x) {
	s[++top]=x;for(int i=x;!isroot(i);i=f[i]) s[++top]=f[i];
	while(top) pd(s[top--]);
	while(!isroot(x)) {
		if(!isroot(f[x])) {
			if(is(x)^is(f[x])) spin(x);
			else spin(f[x]);
		}
		spin(x);
	}
}
void acc(int x) {int y=0;while(x) splay(x),son[x][1]=y,up(x),y=x,x=f[x];}
void evert(int x) {acc(x),splay(x),rev[x]^=1;}
void link(int x,int y) {evert(x),f[x]=y;}
void split(int x,int y) {evert(y),acc(x),splay(x);}
void cut(int x,int y) {split(x,y);son[x][0]=f[y]=0,up(x);}
int query(int x,int y) {split(x,y);return mx[x];}
int main()
{
	n=read(),m=read();
	for(int i=1;i<=n+m;++i) p[i]=mx[i]=i;
	for(int i=1;i<=m;++i)
		e[i].x=read(),e[i].y=read(),e[i].a=read(),e[i].b=read();
	sort(e+1,e+1+m,cmp);
	for(int i=1;i<=m;++i) {
		v[i+n]=e[i].b;
		int r1=find(e[i].x),r2=find(e[i].y);
		if(r1!=r2) p[r1]=r2,link(e[i].x,i+n),link(e[i].y,i+n);
		else {
			int kl=query(e[i].x,e[i].y);
			if(v[kl]>e[i].b) {//删边,加边
				cut(e[kl-n].x,kl),cut(e[kl-n].y,kl);
				link(e[i].x,i+n),link(e[i].y,i+n);
			}
		}
		if(find(1)==find(n)) ans=min(ans,e[i].a+v[query(1,n)]);
	}
	if(ans<inf) printf("%d",ans);
	else puts("-1");
	return 0;
}

扩展

维护子树信息

我:树王树王,LCT能做什么?
树王:树剖能做的事,LCT都能做。
我:那维护子树信息呢?
树王:也能。

例题:洛谷P4219/bzoj4530 大融合
最大的一个问题就是,LCT上,虚边连接的两个点是儿子认父亲但是父亲不认儿子。所以此时我们对每个节点维护两个东西,一个叫做sz,是以该点为根的子树的大小,另一个叫做ksz,是该点没有用虚边连接的子树的大小。sz包含有ksz。
每次pushup,就让sz[x]=sz[son[x][0]]+sz[son[x][1]]+ksz[x]+1;
至于这个ksz怎么维护…主要有这么几个地方和普通的LCT不同。
1.access的时候

void acc(int x) {
	int y=0;
	while(x) splay(x),ksz[x]+=sz[s[x][1]]-sz[y],s[x][1]=y,up(x),y=x,x=f[x];
}

因为此时s[x][1]与x之间的边变成了虚边。当然y的贡献在实边贡献里,要删掉。
2.link的时候
从前我们link,只用evert(x),然后让y当x的父亲即可。这样的连接连的虚边,所以自然ksz[y]+=sz[x],可是y的那些用虚边连起来的父亲们就需要获得却无法获得这个加上来的sz[x]。所以我们的link应该要先evert(x)一下,再access(y)一下才行。

至于最后的答案呢,就是split之后的(ksz[x]+1)*(ksz[y]+1),因为split之后,x和y之间只有一条实边相连。

#include<bits/stdc++.h>
using namespace std;
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
#define RI register int
const int N=100005;
int n,m,top,bj;
int sz[N],ksz[N],f[N],s[N][2],rev[N],st[N];//sz:总信息 ksz
int isroot(int x) {return s[f[x]][0]!=x&&s[f[x]][1]!=x;}
int is(int x) {return s[f[x]][1]==x;}
void pd(int x) {
	if(!rev[x]) return;
	if(s[x][0]) rev[s[x][0]]^=1;
	if(s[x][1]) rev[s[x][1]]^=1;
	swap(s[x][0],s[x][1]),rev[x]=0;
}
void up(int x) {sz[x]=ksz[x]+1+sz[s[x][0]]+sz[s[x][1]];}
void spin(int x) {
	int fa=f[x],g=f[fa],t=is(x);
	if(!isroot(fa)) s[g][is(fa)]=x;
	f[fa]=x,f[x]=g,f[s[x][t^1]]=fa;
	s[fa][t]=s[x][t^1],s[x][t^1]=fa;
	up(fa),up(x);
}
void splay(int x) {
	st[++top]=x;for(RI i=x;!isroot(i);i=f[i]) st[++top]=f[i];
	while(top) pd(st[top--]);
	while(!isroot(x)) {
		if(!isroot(f[x])) {
			if(is(f[x])^is(x)) spin(x);
			else spin(f[x]);
		}
		spin(x);
	}
}
void acc(int x) {
	int y=0;
	while(x) splay(x),ksz[x]+=sz[s[x][1]]-sz[y],s[x][1]=y,up(x),y=x,x=f[x];
}
void evert(int x) {acc(x),splay(x),rev[x]^=1;}
void split(int x,int y) {evert(x),acc(y),splay(y);}
void link(int x,int y) {split(x,y),f[x]=y,ksz[y]+=sz[x],up(y);}
int main()
{
	char ch[10];int x,y;
	n=read(),m=read();
	for(RI i=1;i<=n;++i) sz[i]=1;
	while(m--) {
		scanf("%s",ch),x=read(),y=read();
		if(ch[0]=='A') link(x,y);
		else bj=1,split(x,y),printf("%lld\n",1LL*(ksz[y]+1)*(ksz[x]+1));
	}
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值