LCT总结——应用篇(附题单)(LCT)
一般都是维护链的操作。split即可搞定。
进阶操作的话,处理好辅助树和原树的关系即可搞定。
其实,最大的区别就是,splay随便转,辅助树形态变了,但是原树形态不变,makert会让原树形态变化
LCT维护子树信息
真儿子会splay的时候各种变化,但是虚儿子只会在access和link的时候发生变化,其他的时候可以理解为跟着转。
以处理子树sz为例,
处理虚边子树sz,总sz(包括实边)两个
pushup注意下。
access和link注意下。
需要真正找子树信息的时候,考虑把x的后继access掉,把xsplay到根,然后虚边子树的sz就是子树sz了。(前提,makert的根不在x子树里)
模板题:
[BJOI2014]大融合
注意查询x,y的子树大小的话,makert(x),access(y),splay(y).
其实x,y直接连通,所以现在是一个长度为2的链,y是根,x是y的右儿子,然后直接(si[y]+1)*(si[x]+1)即可。(si[x]+1可以换成s[x])
#include#define il inline #define reg register int #define numb (ch^'0') using namespace std; typedef long long ll; il void rd(int &x){ char ch;bool fl=false; while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true); for(x=numb;isdigit(ch=getchar());x=x*10+numb); (fl==true)&&(x=-x); } namespace Miracle{ const int N=1e5+5; int ch[N][2],fa[N],si[N],s[N]; int r[N]; int n,q; bool nrt(int x){ return (ch[fa[x]][0]==x)||(ch[fa[x]][1]==x); } void pushup(int x){ if(x) s[x]=s[ch[x][0]]+s[ch[x][1]]+si[x]+1; } void rev(int x){ swap(ch[x][0],ch[x][1]); r[x]^=1; } void pushdown(int x){ if(r[x]){ rev(ch[x][0]);rev(ch[x][1]); r[x]=0; } } void rotate(int x){ int y=fa[x],d=ch[fa[x]][1]==x; fa[ch[y][d]=ch[x][!d]]=y; if(nrt(y)) ch[fa[x]=fa[y]][ch[fa[y]][1]==y]=x; else fa[x]=fa[y]; ch[fa[y]=x][!d]=y; pushup(y); } int sta[N],top; void splay(int x){ int y=x; sta[++top]=y; while(nrt(y)) y=fa[y],sta[++top]=y; while(top) pushdown(sta[top--]); int z; while(nrt(x)){ y=fa[x],z=fa[y]; if(nrt(y)){ rotate(((ch[z][0]==y)==(ch[y][0]==x))?y:x); } rotate(x); } pushup(x); } void access(int x){ for(int y=0;x;y=x,x=fa[x]){ splay(x); si[x]-=s[y]; si[x]+=s[ch[x][1]]; ch[x][1]=y; pushup(x); } } void makert(int x){ access(x); splay(x); rev(x); } void link(int x,int y){ makert(x); makert(y); si[y]+=s[x]; s[y]+=s[x]; fa[x]=y; pushup(y); } int query(int x,int y){ makert(x);access(y); return (long long)(si[x]+1)*(si[y]+1); } int main(){ rd(n);rd(q); char cc[2]; int x,y; for(reg i=1;i<=n;++i){ si[i]=0;s[i]=1; } while(q--){ scanf("%s",cc+1); rd(x);rd(y); if(cc[1]=='A'){ link(x,y); }else{ printf("%d\n",query(x,y)); } } return 0; } } signed main(){ Miracle::main(); return 0; } /* Author: *Miracle* Date: 2018/12/18 11:13:18 */
LCT维护联通块信息(边双联通分量)
每个LCT的点,是一个边双。只不过存在一些名存实亡的点。
只能支持插入边,维护这个边双连通分量缩点之后得到的树
外面要再用一个并查集维护每个点所在的边双的代表节点。即并查集树的树根。
(这里又多了一个并查集树,可以理解为,就是为了缩点打的懒标记而已(否则暴力缩点岂不是O(n)的?))
(upda:2019.2.14:更好的解释是,其实我们扫右儿子的子树的时候,已经完成了缩点,但是右儿子的虚儿子没有记录,不能直接指向x,所以只能“懒惰”用并查集先记录一下了)
合并缩环的时候,类似于打标记,把这个实链的点的father暴力指向x,然后干掉x的右儿子,这些部分就不存在了。
名存实亡。都用x代替了。
然后pushup(x)
但是,可能这些实链的子树,还必须要继承到x上,怎么办呢?
由于类似于打标记,先不继承它,
因为每次处理对于链或者连通性的询问都要access,所以在这个上面做文章。
access的时候,跳father用并查集直接跳到边双的代表点p。然后顺便把自己的辅助树的father设为p。
这样,就会把子树继承上去了,并且真正意义上实现了缩点,
使得边双之间用桥连接。
由于有并查集,所以每次都要x=fin(x)找到真正的点再处理。
看似复杂,其实理解上,就是多了一些个用并查集维护的懒标记,用到的时候及时还原即可。
(但是对于维护边双联通分量,还要维护缩完点后的树的子树信息的话,,,,好像子树不能及时继承上去,估计就比较难办了。。2333)
(upda:2019.2.14:上面那个说的不对,子树信息当然可以做,因为缩点对于最高点的x的子树信息没有影响的。)
(upda:2019.3.13:emm,要看维护什么子树信息了。sz的话是不虚的,但是具体距离什么的还是没法办)
例题1:[AHOI2005]航线规划——LCT维护边双联通分量
例题2:【bzoj2959】长跑
维护的sz变成sz[x]=sz[ls]+sz[rs]+my[x]
my[x]是自己边双内部的刷卡机个数
对于2操作,找到真正的边双点,my[root]+=new-old[x],old[x]=new;
操作3直接查询链上的sz即可。
#include#define il inline #define reg register int #define numb (ch^'0') #define ls t[x].ch[0] #define rs t[x].ch[1] using namespace std; typedef long long ll; il void rd(int &x){ char ch;bool fl=false; while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true); for(x=numb;isdigit(ch=getchar());x=x*10+numb); (fl==true)&&(x=-x); } namespace Miracle{ const int N=150000+5; int n,m; struct node{ int sz,fa,ch[2],r; int my; }t[N]; int fa[N]; int fin(int x){ return fa[x]==x?x:fa[x]=fin(fa[x]); } bool nrt(int x){ return (t[t[x].fa].ch[0]==x)||(t[t[x].fa].ch[1]==x); } void rev(int x){ swap(t[x].ch[0],t[x].ch[1]); t[x].r^=1; } void pushdown(int x){ if(t[x].r){ t[x].r=0; rev(ls);rev(rs); } } void pushup(int x){ if(x) t[x].sz=t[ls].sz+t[rs].sz+t[x].my; } void rotate(int x){ int y=t[x].fa,d=t[y].ch[1]==x; t[t[y].ch[d]=t[x].ch[!d]].fa=y; if(nrt(y)) t[t[x].fa=t[y].fa].ch[t[t[y].fa].ch[1]==y]=x; else t[x].fa=t[y].fa; t[t[y].fa=x].ch[!d]=y; pushup(y); } int sta[N]; void splay(int x){ int y=x,z=0; sta[++z]=y; while(nrt(y)) y=t[y].fa,sta[++z]=y; while(z) pushdown(sta[z--]); while(nrt(x)){ y=t[x].fa,z=t[y].fa; if(nrt(y)){ rotate(((t[y].ch[0]==x)==(t[z].ch[0]==y))?y:x); } rotate(x); } pushup(x); } void access(int x){ for(reg y=0;x;y=x,x=t[y].fa=fin(t[x].fa)){ splay(x);rs=y;pushup(x); } } void makert(int x){ access(x);splay(x);rev(x); } int findrt(int x){ access(x);splay(x); pushdown(x); while(t[x].ch[0]) pushdown(x=t[x].ch[0]); splay(x); return x; } void dele(int x,int y){ if(x) fa[x]=y,t[y].my+=t[x].my,dele(ls,y),dele(rs,y); } void merge(int x,int y){ if(x==y) return; makert(x); if(findrt(y)!=x){ t[x].fa=y; pushup(y); return; } dele(t[x].ch[1],x); t[x].ch[1]=0; pushup(x); } void split(int x,int y){ makert(x);access(y);splay(y); } int query(int x,int y){ makert(x); if(findrt(y)!=x) return -1; splay(y); return t[y].sz; } int now[N]; int main(){ rd(n);rd(m); int x,y; for(reg i=1;i<=n;++i){ rd(x); now[i]=x; fa[i]=i;t[i].sz=x;t[i].my=x; } int c; for(reg i=1;i<=m;++i){ rd(c);rd(x);rd(y); if(c==1){ x=fin(x);y=fin(y); merge(x,y); }else if(c==2){ int tmp=y-now[x]; //cout<<" tmp "< now[x]=y; x=fin(x); //cout<<" xx "< //splay(x); makert(x); t[x].my+=tmp; pushup(x); }else{ printf("%d\n",query(fin(x),fin(y))); } } return 0; } } signed main(){ Miracle::main(); return 0; } /* Author: *Miracle* Date: 2018/12/19 9:57:28 */
LCT维护边权(常用生成树)
边,直接变成点,因为LCT不能直接维护边。
生成树边的信息记录在点上。
水管局长(动态最小生成树)
严格次小生成树(也可以不用LCT)
最小差值生成树:
肯定要把边排序,枚举固定一个较大值,
暴力的做法是不断把小的加进去,直到成为一个联通块。
考虑是否能用到上一次处理的信息,
不断加入值的时候,如果成环,那么把环上最小值替换掉即可。
这样,通过r-1的最小差值生成树,删除并添加一条边,就可以得到r的最小差值生成树
正确性的话,
如果存在一个更靠后的L使得[L,R]是生成树,那么必然是可以用R之前的边把一些小的边替换掉,之前肯定已经处理这种情况了。
代码:
注意:
1.pushup更新值的时候,不严格优秀就可以更新,不能只取<,否则在相等的时候会错误。
2.处理x,y的链的时候,时刻注意谁在上面,access(y)可能y就变到了x的祖先。以及findrt(y)也可能会。
// luogu-judger-enable-o2 #include#define il inline #define reg register int #define numb (ch^'0') #define ls t[x].ch[0] #define rs t[x].ch[1] using namespace std; typedef long long ll; il void rd(int &x){ char ch;bool fl=false; while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true); for(x=numb;isdigit(ch=getchar());x=x*10+numb); (fl==true)&&(x=-x); } namespace Miracle{ const int N=50000+5; const int M=200000+5; const int inf=0x3f3f3f3f; int n,m; int tot; int ptr; struct node{ int ch[2],fa,r; int mi,id; int val; int from; }t[N+M]; bool nrt(int x){ return (t[t[x].fa].ch[0]==x)||(t[t[x].fa].ch[1]==x); } void rev(int x){ swap(t[x].ch[0],t[x].ch[1]); t[x].r^=1; } void pushup(int x){ if(t[x].val<=t[ls].mi&&t[x].val<=t[rs].mi){ t[x].mi=t[x].val; t[x].id=x; } else if(t[ls].mi<=t[x].val&&t[ls].mi<=t[rs].mi){ t[x].mi=t[ls].mi; t[x].id=t[ls].id; } else{ t[x].mi=t[rs].mi; t[x].id=t[rs].id; } } void pushdown(int x){ if(t[x].r){ rev(t[x].ch[1]),rev(t[x].ch[0]); t[x].r=0; } } void rotate(int x){ int y=t[x].fa,d=t[y].ch[1]==x; t[t[y].ch[d]=t[x].ch[!d]].fa=y; if(nrt(y)) t[t[x].fa=t[y].fa].ch[t[t[y].fa].ch[1]==y]=x; else t[x].fa=t[y].fa; t[t[x].ch[!d]=y].fa=x; pushup(y); } int sta[N]; void splay(int x){ int y=x,z=0; sta[++z]=y; while(nrt(y)) y=t[y].fa,sta[++z]=y; while(z) pushdown(sta[z--]); while(nrt(x)){ y=t[x].fa,z=t[y].fa; if(nrt(y)){ rotate(((t[z].ch[1]==y)==(t[y].ch[1]==x))?y:x); } rotate(x); } pushup(x); } void access(int x){ for(reg y=0;x;y=x,x=t[x].fa){ splay(x); t[x].ch[1]=y; pushup(x); } } void makert(int x){ //cout<<" making "< access(x); //cout<<" access end "< splay(x); //cout<<" splay end "< rev(x); } int findrt(int x){ access(x);splay(x); while(t[x].ch[0]) x=t[x].ch[0]; splay(x); return x; } int c=0; bool in[M]; void link(int x,int y,int z,int fr){ // cout<<" link "< t[tot].val=z;t[tot].id=tot,t[tot].mi=z;t[tot].from=fr; // cout<<" t[4] "< in[fr]=1; makert(x); // cout<<" sdfsdfj "< // cout< // cout<<" over "< if(findrt(y)!=x){ // cout<<" add new "< t[x].fa=tot; t[tot].fa=y; // pushup(tot); // pushup(y); ++c; return; } pushup(x); // cout< int lp=t[x].id; // cout<<" kil "< in[t[lp].from]=0; splay(lp); t[t[lp].ch[0]].fa=0; t[t[lp].ch[1]].fa=0; makert(x); t[x].fa=tot; t[tot].fa=y; } struct edge{ int x,y,z; bool friend operator <(edge a,edge b){ return a.z<b.z; } }e[M]; int main(){ rd(n);rd(m); int x,y,z; for(reg i=1;i<=m;++i){ rd(x);rd(y);rd(z); e[i].x=x;e[i].y=y;e[i].z=z; } sort(e+1,e+m+1); tot=n; int ans=0x3f3f3f3f; t[0].val=inf,t[0].mi=inf,t[0].id=-1; for(reg i=1;i<=n;++i){ t[i].val=inf;t[i].mi=inf;t[i].id=-1; } for(reg i=1;i<=m;++i){ // cout<<" iiii "< ++tot; if(e[i].x!=e[i].y) link(e[i].x,e[i].y,e[i].z,i); while(in[ptr]==0) ++ptr; if(c==n-1){ // cout<<" ok "< // cout<<" ptr "< ans=min(ans,e[i].z-e[ptr].z); } } printf("%d",ans); return 0; } } signed main(){ Miracle::main(); return 0; } /* Author: *Miracle* Date: 2018/12/19 8:17:20 */
LCT维护树上染色联通块
[SDOI2017]树点涂色
到根路径的染色好似access操作,而且每种颜色都是新出现的,并且每一种颜色的地位相同。
用LCT维护颜色块的集合,只用到access操作,再用一个线段树维护dfs序,num[x]表示x点到根节点的路径颜色数目。
只有在虚边变成实边的时候,才会发生真正意义上的颜色改变,会对子树答案造成影响,
具体来说,变换重儿子的时候,把原来的位置的子树的每个num加上1,新儿子的num每个减掉1
操作2,num[a]+num[b]-2*num[lca]+1(分类讨论一下即可证明)
操作3,区间求max
这个题,LCT不算是用来维护动态树的工具,只是因为access和操作1非常相似,从而进行了一个“建模”,用LCT来拟合这个操作。
#include#define il inline #define reg register int #define numb (ch^'0') #define mid ((l+r)>>1) using namespace std; typedef long long ll; il void rd(int &x){ char ch;bool fl=false; while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true); for(x=numb;isdigit(ch=getchar());x=x*10+numb); (fl==true)&&(x=-x); } namespace Miracle{ const int N=1e5+5; const int inf=0x3f3f3f3f; int mx[4*N],ad[4*N]; int n,m; struct edge{ int nxt,to; }e[2*N]; struct node{ int fa,ch[2]; }t[N]; int hd[N],cnt; void add(int x,int y){ e[++cnt].nxt=hd[x]; e[cnt].to=y; hd[x]=cnt; } int dfn[N],dfn2[N],df; int fdfn[N],dep[N]; int fa[N][20]; void dfs(int x,int d){ dfn[x]=++df; fdfn[df]=x; dep[x]=d; for(reg i=hd[x];i;i=e[i].nxt){ int y=e[i].to; if(y==fa[x][0]) continue; t[y].fa=x; fa[y][0]=x; dfs(y,d+1); } dfn2[x]=df; } void pushup(int x){ mx[x]=max(mx[x<<1],mx[x<<1|1]); } void pushdown(int x){ if(!ad[x]) return; ad[x<<1]+=ad[x]; ad[x<<1|1]+=ad[x]; mx[x<<1]+=ad[x]; mx[x<<1|1]+=ad[x]; ad[x]=0; } void build(int x,int l,int r){ if(l==r){ mx[x]=dep[fdfn[l]];return; } build(x<<1,l,mid);build(x<<1|1,mid+1,r); pushup(x); } void upda(int x,int l,int r,int L,int R,int c){ if(L<=l&&r<=R){ ad[x]+=c;mx[x]+=c;return; } pushdown(x); if(L<=mid) upda(x<<1,l,mid,L,R,c); if(mid 1|1,mid+1,r,L,R,c); pushup(x); } int query(int x,int l,int r,int p){ if(l==r){ return mx[x]; } pushdown(x); if(p<=mid) return query(x<<1,l,mid,p); return query(x<<1|1,mid+1,r,p); } int ask(int x,int l,int r,int L,int R){ if(L<=l&&r<=R){ return mx[x]; } pushdown(x); int ret=0; if(L<=mid) ret=max(ret,ask(x<<1,l,mid,L,R)); if(mid 1|1,mid+1,r,L,R)); return ret; } bool nrt(int x){ return t[t[x].fa].ch[0]==x||t[t[x].fa].ch[1]==x; } void rotate(int x){ int y=t[x].fa,d=t[y].ch[1]==x; t[t[y].ch[d]=t[x].ch[!d]].fa=y; if(nrt(y)) t[t[x].fa=t[y].fa].ch[t[t[y].fa].ch[1]==y]=x; else t[x].fa=t[y].fa; t[t[x].ch[!d]=y].fa=x; } void splay(int x){ while(nrt(x)){ int y=t[x].fa,z=t[y].fa; if(nrt(y)){ rotate(((t[z].ch[1]==y)==(t[y].ch[1]==x))?y:x); } rotate(x); } } int bac(int x){ if(t[x].ch[1]==0) return 0; x=t[x].ch[1]; while(t[x].ch[0]) x=t[x].ch[0]; return x; } int pre(int x){ while(t[x].ch[0]) x=t[x].ch[0]; return x; } void access(int x){ //cout<<" accessing "< for(reg y=0;x;y=x,x=t[x].fa){ //cout<<" x y "< splay(x); int lp=bac(x); //cout<<" bac "< if(lp){ upda(1,1,n,dfn[lp],dfn2[lp],1); } if(y){ lp=pre(y); // cout<<" pre "< upda(1,1,n,dfn[lp],dfn2[lp],-1); } t[x].ch[1]=y; } } int lca(int x,int y){ if(dep[x]<dep[y]) swap(x,y); for(reg j=19;j>=0;--j){ if(dep[fa[x][j]]>=dep[y]){ x=fa[x][j]; } } if(x==y) return x; for(reg j=19;j>=0;--j){ if(fa[x][j]!=fa[y][j]){ x=fa[x][j];y=fa[y][j]; } } return fa[x][0]; } int main(){ rd(n);rd(m); int x,y; for(reg i=1;i 1,1); dep[0]=-1; for(reg j=1;j<=19;++j){ for(reg i=1;i<=n;++i){ fa[i][j]=fa[fa[i][j-1]][j-1]; } } // for(reg i=1;i<=n;++i){ // cout<// } build(1,1,n); int c; while(m--){ rd(c); if(c==1){ rd(x); access(x); } else if(c==2){ rd(x);rd(y); int anc=lca(x,y); // cout<<" anc "<i){ rd(x);rd(y); add(x,y);add(y,x); } dfs( //cout<<" ---- "< printf("%d\n",query(1,1,n,dfn[x])+query(1,1,n,dfn[y])-2*query(1,1,n,dfn[anc])+1); }else{ rd(x); printf("%d\n",ask(1,1,n,dfn[x],dfn2[x])); } } return 0; } } signed main(){ Miracle::main(); return 0; } /* Author: *Miracle* Date: 2018/12/19 20:08:02 */
重组病毒、LCT+SAM查询区间子串问题都是这个思路