LCT

不写替罪羊是有什么心事吗。

lct就是用若干splay维护实链。

实链剖分是一种链剖分。实链是钦定出来的链,我说它是实链他就是实链。但是注意实链深度是单增的,和重长链一个道理。

然后lct同时表示两棵树,一个是原树,原树上的实链就是深度递增的链。另一个是辅助树,是splay表示的树,满足中序遍历的点编号就是实链从上到下的点编号。

其他性质见 oiwiki 和 luogu 题解。

然后说一下众操作。

板题

先放一下码。

#include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
int n,m;
int Val[MAXN];
struct Link_Cut_Tree{
	#define ls(p) tree[p].son[0]
	#define rs(p) tree[p].son[1]
	struct TREE{int fa,son[2],val,rev,ans;}tree[MAXN];
	int stac[MAXN],top;
	inline void push_up(int p){tree[p].ans=tree[ls(p)].ans^tree[rs(p)].ans^tree[p].val;}
	inline void spread(int p){if(!tree[p].rev)return;swap(ls(p),rs(p)),tree[ls(p)].rev^=1,tree[rs(p)].rev^=1,tree[p].rev=0;}
	inline bool isroot(int p){return ls(tree[p].fa)!=p&&rs(tree[p].fa)!=p;}
	inline void rotate(int x){
		int y=tree[x].fa,z=tree[y].fa,k=rs(y)==x,s=tree[x].son[k^1];
		if(!isroot(y))tree[z].son[rs(z)==y]=x;
		tree[x].son[k^1]=y,tree[y].son[k]=s;
		if(s)tree[s].fa=y;tree[y].fa=x,tree[x].fa=z;push_up(y);
	}
	inline void splay(int x){
		int u=x;stac[top=1]=u;while(!isroot(u))stac[++top]=u=tree[u].fa;while(top)spread(stac[top--]);
		while(!isroot(x)){
			int y=tree[x].fa,z=tree[y].fa;
			if(!isroot(y))((ls(y)==x)^(ls(z)==y))?rotate(x):rotate(y);rotate(x);
		}push_up(x);
	}
	inline void access(int x){for(int t=0;x;t=x,x=tree[x].fa)splay(x),rs(x)=t,push_up(x);}
	inline void makeroot(int p){access(p),splay(p),tree[p].rev^=1;}
	inline int find(int p){access(p),splay(p);while(ls(p))p=ls(p);return p;}
	inline void split(int x,int y){makeroot(x),access(y),splay(y);}
	inline void link(int x,int y){makeroot(x),tree[x].fa=y;}
	inline void cut(int x,int y){split(x,y);if(ls(y)==x&&!rs(x))ls(y)=0,tree[x].fa=0;}
}LCT;
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&Val[i]),LCT.tree[i].val=LCT.tree[i].ans=Val[i];
	for(int i=1,opt,x,y;i<=m;i++){
		scanf("%d%d%d",&opt,&x,&y);
		if(!opt)LCT.split(x,y),printf("%d\n",LCT.tree[y].ans);
		else if(opt==1)(LCT.find(x)!=LCT.find(y))?LCT.link(x,y):void();
		else if(opt==2)(LCT.find(x)==LCT.find(y))?LCT.cut(x,y):void();
		else LCT.access(x),LCT.splay(x),LCT.tree[x].val=y,LCT.push_up(x);
	}
	return 0;
}

上下传

参考一般splay的上下传即可,作用一致,板子中维护区间反转。

isroot(x)

lct中每个splay的根节点会以一条虚边的形式指向它原树上的父亲,进而地,这样的父子关系应是单向的,即在该splay根处能查到它不属于本splay的父亲,但在父亲上查不到这个儿子。

利用这个性质,如果一个节点不是它父亲的任一儿子则它是其所属splay的根。

splay(x)&rotate(x)

作用同splay中同名函数作用,实现有不同。根据上文,即使是当前splay的根节点也是有父亲的,所以在rotate和splay操作中都应判断某节点父亲是否属于这个splay,即节点是否为根。

access(x)

是lct的核心操作。作用是把原树上根节点到 x 的路径变成一条实链。

带图博客

显然对于当前的根,x 向上的全部祖先都要变成实边,并且其他无关的实边都应该清空成虚边。

结合一下实链的定义,当前节点的左子树应该是它的祖先集合子集,右子树则是一条儿子链,那对于当前点 x 首先要做的就是旋转到根,清空右儿子(刨去子树中的无关实边)。

这一操作可以保证左儿子(祖先集合)仍然存在实边,此时让 x 变为其于原树上的父亲(直接跳fa)重复此操作即可。

重复这样的操作知道根,中间每个节点清空完记得 pushup 当前节点信息。

想明白定义间的关系后该操作其实十分易懂。

makeroot(x)

显然上面的操作不能处理路径,因为路径可以跨深度。

那我把路径的一头换成根不就不跨深度了。所以需要这个函数换根,换原树的根。

想这样一个事情,access(x) 之后这个splay形成了一条从根到 x 的实链,那么 x 应该是这个splay中序遍历的最后一个点才对,换言之,此时将 x 旋转到根则它不存在右子树且左子树巨大。

那不妨直接给 x 打一个反转标记然后全树反转,这样 x 就全是右儿子了,而且中序遍历反过来了。

split(x,y)

有了上面两个操作就可以扯下来一条路径了,对于有向路径 xyx makeroot然后access y 即可。完了之后想访问路径信息就把 y splay了,直接访问节点 y

findroot(x)

lct可以维护森林,所以图不一定联通,所以查询节点于其原树上的树根是有必要的。

方法是容易的,access(x) 后 splay,跳左儿子即可。跳完记得在旋转一下保证复杂度。

link(x,y)

猜它为啥叫 link-cut-tree。

把任一节点 makeroot后钦定父亲为另一节点即可。

cut(x,y)

猜它为啥叫 link-cut-tree。

因为这是一条边,所以makeroot一节点后access另一节点并旋转到根,此时两点于splay上的关系应恰为父亲和左儿子的关系,断掉这个父子关系即可。

另外如果 x makeroot了 y splay后 x 应该没有右子树,要不然断的就不是边。

注意事项

makeroot中的反转标记应该考虑在什么时候,如何下放。

具体地,在 splay(x) 开始之前就应该获得这个splay于x的整个祖先链并从上到下地下方标记,这个写个栈就能实现。

洞穴勘测

find()。

树的维护

显然这是一道树剖板子,所以用lct做。

问题在于lct表示不了边,这种情况一般建立边代表的点。规定边编号 Eid(i)=i+n,连边则可以将两端点与边点 link(),再在边点上维护边信息,图点只作为连接使用正常跑lct即可。

Tree II

这不是我们线段树2的梗吗怎么跑到这了。

pushup的时候顺带维护一下节点的siz就可以处理区间加的和维护了,tag的维护参考线段树2就行。

最小差值生成树

考虑将边权排序后双指针。具体地,对于新加入的这条最大边,为了保证树的结构会cut掉一些小边,用 multiset 存储边权,LCT中则存储当前联通块的最小边权对应的边编号,贪心拆。

对于本就不联通的点说明当前情况不是一棵树,所以连边后直接将答案赋值,否则对答案取最小。

水管局长

有了上一道题的经验简单了很多。考虑用lct维护一个图的最小生成树,并维护最大边的编号,查询的时候 split() 出来输出权值即可。

但是删边的操作难以处理,考虑离线反向处理改加边即可。

具体地,将边按权值排序后对于最终剩下的边可以在一开始就贪心维护出最小生成树。加边途中如果这条边 w 连接的两点 (u,v) 原先路径的最大边权 w>w 则删边并替换。

[Codechef MARCH14] GERALD07

有一个不要脑子的莫队 O(nlognn) 做法,但是这题强制在线。

想这样一个问题,在询问区间内的边越多,联通块应当越少。考虑用lct维护询问与边集连通性等效的生成森林,则每次加入边可以断掉一个编号最小(大)的边。

进一步地,用主席树维护每次加边顶掉的边的编号,由于是生成树所以节点数-边数=联通块数,答案即 n(rl+1query(l,r))

query(l,r) 表示连到 r 时顶掉了多少编号在 [l,r] 内的边。

在美妙的数学王国中畅游

最破防的一集。

根据所学:一次函数从二阶导开始为0,f(x)=ex 任意阶导数为其自身,f(x)=sin(x) 的导数有长为 4 的循环节:sin(x)=cos(x),cos(x)=sin(x),sin(x)=cos(x),cos(x)=sin(x),结合题目给出的公式可以简单维护。

又因为本题有精度限制,将多项式迭代到 X=20 即可,即单点的求值是O(X) 的,剩下就是lct板子。

另外操作magic时应先把 c 旋转到根。此事在我的讨论和犇犇中亦有记载。

航线规划

这个是LCT维护双连通分量,网上写的都好丑。

答案就是给图中的双连通分量缩点后路径上的点个数,这样的话得存一下每个点所属的边双,用并查集存,并且在 access 中把父亲直接改成其所属边双。

连边的时候发现成环了就直接重构子树。其他没有变化。

	inline void access(int x){for(int t=0;x;t=x,x=tree[x].fa=getf(tree[x].fa))splay(x),rs(x)=t,push_up(x);}
	inline void makeroot(int x){access(x),splay(x),tree[x].rev^=1;}
	inline int find(int x){access(x),splay(x);while(ls(x))spread(x),x=ls(x);spread(x);splay(x);return x;}
	inline void split(int x,int y){makeroot(x),access(y),splay(y);}
	inline void dfs(int u,int f){
		if(!u)return ;
		father[u]=f;
		dfs(ls(u),f),dfs(rs(u),f);
	} 
	inline void link(int x,int y){
		x=getf(x),y=getf(y);
		if(x==y)return;
		makeroot(x);
		if(find(x)!=find(y))tree[x].fa=y;
		else{
			dfs(rs(x),x);
			rs(x)=0;push_up(x);
		}
	}

决战

乍一看十分一眼啊,不就lct板子吗。实则暗藏玄机,操作五是不能打个rev就完事的,因为要不然和换个根没区别,再一想当前lct压根就没法维护这个东西。

所以需要用两个lct分别维护树的形态和权值。

#include<bits/stdc++.h>
#define MAXN 100005
#define int long long
using namespace std;
int n,q,_;
struct Link_Cut_Tree{
	#define ls(p) tree[p].son[0]
	#define rs(p) tree[p].son[1]
	struct TREE{int fa,son[2],val,xv,nv,ans,rev,tag,siz,pos;}tree[MAXN];int stac[MAXN],top;
	inline void push_up(int p){
		tree[p].xv=max({tree[ls(p)].xv,tree[rs(p)].xv,tree[p].val});
		tree[p].nv=min({tree[ls(p)].nv,tree[rs(p)].nv,tree[p].val});
		tree[p].ans=tree[ls(p)].ans+tree[rs(p)].ans+tree[p].val;
		tree[p].siz=tree[ls(p)].siz+tree[rs(p)].siz+1;
	}
	inline void down(int p,int k){tree[p].val+=k,tree[p].ans+=k*tree[p].siz;tree[p].tag+=k;tree[p].xv+=k,tree[p].nv+=k;}
	inline void spread(int p){
		if(tree[p].rev){swap(ls(p),rs(p));tree[ls(p)].rev^=1,tree[rs(p)].rev^=1,tree[p].rev=0;}
		if(tree[p].tag){down(ls(p),tree[p].tag),down(rs(p),tree[p].tag),tree[p].tag=0;}
	}
	inline bool isroot(int p){return p!=ls(tree[p].fa)&&p!=rs(tree[p].fa);}
	inline void rotate(int x){
		int y=tree[x].fa,z=tree[y].fa,k=rs(y)==x,s=tree[x].son[k^1];
		if(!isroot(y))tree[z].son[rs(z)==y]=x;
		else tree[x].pos=tree[y].pos;
		tree[x].son[k^1]=y,tree[y].son[k]=s;
		if(s)tree[s].fa=y;tree[y].fa=x,tree[x].fa=z;push_up(y),push_up(x);
	}
	inline void splay(int x){
		int u=x;stac[top=1]=u;while(!isroot(u))stac[++top]=u=tree[u].fa;while(top)spread(stac[top--]);
		while(!isroot(x)){
			int y=tree[x].fa,z=tree[y].fa;
			if(!isroot(y))((ls(y)==x)^(ls(z)==y))?rotate(x):rotate(y);rotate(x);
		}push_up(x);
	}
	inline int kth(int x,int k){
		spread(x);
		if(k==tree[ls(x)].siz+1)return x;
		if(k<=tree[ls(x)].siz)return kth(ls(x),k);
		else return kth(rs(x),k-tree[ls(x)].siz-1);
	}
	inline void getpos(int x){splay(x);splay(tree[x].pos);tree[x].pos=kth(tree[x].pos,tree[ls(x)].siz+1);splay(tree[x].pos);}
	inline void access(int x){
		for(int t=0;x;t=x,x=tree[x].fa){
			getpos(x);
			tree[rs(x)].pos=rs(tree[x].pos);
			rs(x)=t;tree[rs(tree[x].pos)].fa=0;
			rs(tree[x].pos)=tree[t].pos;tree[tree[t].pos].fa=tree[x].pos;
			push_up(x);push_up(tree[x].pos);
		}
	}
	inline void makeroot(int x){access(x),getpos(x),tree[x].rev^=1;tree[tree[x].pos].rev^=1;}
	inline void split(int x,int y){makeroot(x),access(y),getpos(y);}
	inline void link(int x,int y){makeroot(x),getpos(y),tree[x].fa=y;}
	inline void update(int x,int y,int k){split(x,y);down(tree[y].pos,k);}
	inline void modify(int x,int y){split(x,y),tree[tree[y].pos].rev^=1;}
	inline int squery(int x,int y){split(x,y);return tree[tree[y].pos].ans;}
	inline int xquery(int x,int y){split(x,y);return tree[tree[y].pos].xv;}
	inline int nquery(int x,int y){split(x,y);return tree[tree[y].pos].nv;}
}LCT;
char opt[10];
const int inf=1e18;
signed main(){
	scanf("%lld%lld%lld",&n,&q,&_);	
	LCT.tree[0].xv=-inf,LCT.tree[0].nv=inf;
	for(int i=1;i<=n;i++)LCT.tree[i].pos=i+n,LCT.tree[i+n].pos=i;
	for(int i=1;i<=n<<1;i++)LCT.tree[i].siz=1;
	for(int i=1,u,v;i<n;i++){
		scanf("%lld%lld",&u,&v);
		LCT.link(u,v);
	}
	for(int i=1,u,v,w;i<=q;i++){
		scanf("%s%lld%lld",opt+1,&u,&v);
		if(opt[1]=='I'&&opt[3]=='c'){
			scanf("%lld",&w);
			LCT.update(u,v,w);
		}
		else if(opt[1]=='S')printf("%lld\n",LCT.squery(u,v));
		else if(opt[1]=='M'&&opt[2]=='a')printf("%lld\n",LCT.xquery(u,v));
		else if(opt[1]=='M'&&opt[2]=='i')printf("%lld\n",LCT.nquery(u,v));
		else if(opt[1]=='I'&&opt[3]=='v')LCT.modify(u,v);
	}
	return 0;
}

大森林

手法挺多的一道题。

先要想明白这些事情:

  • 只要树上两个节点间的路径存在了,则后续加点不会影响这条路径。

  • 在此之上,可以先处理所有加点操作在处理所有询问

  • 显然不能维护 n 棵树实际的生长状态。考虑从树的差异入手:一段操作 [l,r] 可以认为从左到右扫过森林并于 l 处修改,r+1 处撤销。

所以有这样一种思路:把所有操作按照位置为第一关键字,操作>询问为第二关键字,时间为第三关键字排序。从左到右扫过整个森林,通过不断修改一棵树来跑出所有树的终态。

但是仍有问题,这样一个过程每次要把一堆节点换父亲,复杂度就爆了,考虑如何优化这个过程。

然后就有一个建虚点的牛逼处理。对于两次 1 操作间的若干 0 操作,让新生长出来的点全都连到一个虚点上,跑完这一段之后直接把虚点接到生长出点应该在的那个点上就好了。

然后这个题好像是建虚点后makeroot就会破坏树形态,所以得用不带换根的lct,具体见代码。

#include<bits/stdc++.h>
#define MAXN 300005
using namespace std;
int n,m,q;
struct Que{
	int x,tim,l,r,id;
	bool operator<(const Que &a)const{
		if(x==a.x)return tim<a.tim;
		return x<a.x;
	}
}que[MAXN];
struct Link_Cut_Tree{
	#define ls(p) tree[p].son[0]
	#define rs(p) tree[p].son[1]
	struct TREE{int fa,son[2],val,sum;}tree[MAXN];
	inline void push_up(int p){tree[p].sum=tree[ls(p)].sum+tree[rs(p)].sum+tree[p].val;}
	inline bool isroot(int p){return p!=ls(tree[p].fa)&&p!=rs(tree[p].fa);}
	inline void rotate(int x){
		int y=tree[x].fa,z=tree[y].fa,k=rs(y)==x,s=tree[x].son[k^1];
		if(!isroot(y))tree[z].son[rs(z)==y]=x;
		tree[x].son[k^1]=y;tree[y].son[k]=s;
		if(s)tree[s].fa=y;tree[y].fa=x,tree[x].fa=z;push_up(y);
	}
	inline void splay(int x){
		while(!isroot(x)){
			int y=tree[x].fa,z=tree[y].fa;
			if(!isroot(y))((rs(y)==x)^(rs(z)==y))?rotate(x):rotate(y);rotate(x);
		}push_up(x);
	}
	inline int access(int x){int y=0;for(;x;y=x,x=tree[x].fa)splay(x),rs(x)=y,push_up(x);return y;}
	inline void link(int x,int y){splay(x),tree[x].fa=y;}
	inline void cut(int x){access(x),splay(x),ls(x)=tree[ls(x)].fa=0;push_up(x);}
}LCT;
int Rp,loc=2,refl[MAXN],tot=2;
int L[MAXN],R[MAXN];
int ans[MAXN],idx;
signed main(){
	scanf("%d%d",&n,&m);
	L[1]=1,R[1]=n;
	Rp=1;LCT.tree[1].val=1;LCT.push_up(1);
	refl[1]=1;
	LCT.link(loc,1);
	for(int i=1,opt,l,r,x;i<=m;i++){
		scanf("%d%d%d",&opt,&l,&r);
		if(opt==0){
			++tot;
			refl[++Rp]=tot;
			LCT.link(refl[Rp],loc);
			LCT.tree[tot].val=1,LCT.push_up(tot);
			L[Rp]=l,R[Rp]=r;
		}
		else if(opt==1){
			scanf("%d",&x);
			l=max(L[x],l),r=min(R[x],r);
			if(l>r)continue;
			++tot;
			LCT.link(tot,loc);
			que[++q]=(Que){l,i,tot,refl[x],0};
			que[++q]=(Que){r+1,i,tot,loc,0};
			loc=tot;
		}
		else{
			scanf("%d",&x);
			que[++q]=(Que){l,i+m,refl[r],refl[x],++idx};
		}
	}
	sort(que+1,que+1+q);
	for(int i=1;i<=q;i++){
		int x=que[i].x,T=que[i].tim;
		if(T<=m){
			int u=que[i].l,v=que[i].r;
			LCT.cut(u);
			LCT.link(u,v);
		}
		else{
			int u=que[i].l,v=que[i].r,res=0;
			LCT.access(u),LCT.splay(u);
			res+=LCT.tree[u].sum;
			int anc=LCT.access(v);LCT.splay(v);
			res+=LCT.tree[v].sum;
			LCT.access(anc);
			res-=2*LCT.tree[anc].sum;
			ans[que[i].id]=res;
		}
	}
	for(int i=1;i<=idx;i++)printf("%d\n",ans[i]);
	return 0;
}

写数据结构再也不压行了。

posted @   Cl41Mi5deeD  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示