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的核心操作。作用是把原树上根节点到
显然对于当前的根,
结合一下实链的定义,当前节点的左子树应该是它的祖先集合子集,右子树则是一条儿子链,那对于当前点
这一操作可以保证左儿子(祖先集合)仍然存在实边,此时让
重复这样的操作知道根,中间每个节点清空完记得 pushup 当前节点信息。
想明白定义间的关系后该操作其实十分易懂。
makeroot(x)
显然上面的操作不能处理路径,因为路径可以跨深度。
那我把路径的一头换成根不就不跨深度了。所以需要这个函数换根,换原树的根。
想这样一个事情,access(x) 之后这个splay形成了一条从根到
那不妨直接给
split(x,y)
有了上面两个操作就可以扯下来一条路径了,对于有向路径
findroot(x)
lct可以维护森林,所以图不一定联通,所以查询节点于其原树上的树根是有必要的。
方法是容易的,access(x) 后 splay,跳左儿子即可。跳完记得在旋转一下保证复杂度。
link(x,y)
猜它为啥叫 link-cut-tree。
把任一节点 makeroot后钦定父亲为另一节点即可。
cut(x,y)
猜它为啥叫 link-cut-tree。
因为这是一条边,所以makeroot一节点后access另一节点并旋转到根,此时两点于splay上的关系应恰为父亲和左儿子的关系,断掉这个父子关系即可。
另外如果
注意事项
makeroot中的反转标记应该考虑在什么时候,如何下放。
具体地,在 splay(x) 开始之前就应该获得这个splay于x的整个祖先链并从上到下地下方标记,这个写个栈就能实现。
题
find()。
显然这是一道树剖板子,所以用lct做。
问题在于lct表示不了边,这种情况一般建立边代表的点。规定边编号 link()
,再在边点上维护边信息,图点只作为连接使用正常跑lct即可。
这不是我们线段树2的梗吗怎么跑到这了。
pushup的时候顺带维护一下节点的siz就可以处理区间加的和维护了,tag的维护参考线段树2就行。
考虑将边权排序后双指针。具体地,对于新加入的这条最大边,为了保证树的结构会cut掉一些小边,用 multiset
存储边权,LCT中则存储当前联通块的最小边权对应的边编号,贪心拆。
对于本就不联通的点说明当前情况不是一棵树,所以连边后直接将答案赋值,否则对答案取最小。
有了上一道题的经验简单了很多。考虑用lct维护一个图的最小生成树,并维护最大边的编号,查询的时候 split()
出来输出权值即可。
但是删边的操作难以处理,考虑离线反向处理改加边即可。
具体地,将边按权值排序后对于最终剩下的边可以在一开始就贪心维护出最小生成树。加边途中如果这条边
有一个不要脑子的莫队
想这样一个问题,在询问区间内的边越多,联通块应当越少。考虑用lct维护询问与边集连通性等效的生成森林,则每次加入边可以断掉一个编号最小(大)的边。
进一步地,用主席树维护每次加边顶掉的边的编号,由于是生成树所以节点数-边数=联通块数,答案即
最破防的一集。
根据所学:一次函数从二阶导开始为0,
又因为本题有精度限制,将多项式迭代到
另外操作magic时应先把
这个是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;
}
手法挺多的一道题。
先要想明白这些事情:
-
只要树上两个节点间的路径存在了,则后续加点不会影响这条路径。
-
在此之上,可以先处理所有加点操作在处理所有询问
-
显然不能维护
棵树实际的生长状态。考虑从树的差异入手:一段操作 可以认为从左到右扫过森林并于 处修改, 处撤销。
所以有这样一种思路:把所有操作按照位置为第一关键字,操作>询问为第二关键字,时间为第三关键字排序。从左到右扫过整个森林,通过不断修改一棵树来跑出所有树的终态。
但是仍有问题,这样一个过程每次要把一堆节点换父亲,复杂度就爆了,考虑如何优化这个过程。
然后就有一个建虚点的牛逼处理。对于两次 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;
}
写数据结构再也不压行了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律