Tags:数据结构php
\(LCT\),动态树的一种,又能够\(link\)又能够\(cut\)
引用:http://www.cnblogs.com/zhoushuyu/p/8137553.htmlhtml
[x] P4180 [Beijing2010组队]次小生成树Tree https://www.luogu.org/problemnew/show/P4180ios
[ ] 1.22Wander网络
[ ] P3613 睡觉困难综合征 https://www.luogu.org/problemnew/show/P3613数据结构
I 维护联通性函数
维护两点联通性,较易,例题:Cave 洞穴勘测spa
II 维护树链信息.net
正是因为这个LCT能够代替树链剖分的关于链的操做(关于子树信息是没法作到的,感谢@cjfdf斧正「2018.2.25」)
运用\(split\)操做把\(x\)到\(y\)这条链抠出来操做
例题:【模板】Link Cut Tree
这是\(LCT\)的最大做用之一,几乎在每道题中都有体现
PS:树剖的常数小且相对容易调试,建议能写树剖则写(如“初步”的后三题,没有删边操做)调试
III 维护生成树code
这里较为重要,理解须要时间
加入一条边\((x,y)\)的时候,判断\(x,y\)是否联通,若联通,\(split(x,y)\),判断这条路径上的边权最大值(最小值)和所加入的边的边权的关系,再决定\(continue\)或\(cut\)再\(link\)
int Getmax(int x,int y){return t[x].val>t[y].val?x:y;} void pushup(int x){t[x].id=Getmax(x,Getmax(t[lc].id,t[rc].id));}
IV 维护边双联通份量
这里难懂,慢慢体会
边双联通,其实就是说有两条不想交的路径能够到达
这里表述也不是特别清楚,这两道题的意思是————把环缩点
两道题一句话题意:求x,y路径上点(超级点)的siz(val)之和
相似于\(Tarjan\)缩点,遇到环,暴力DFS把全部点指向一个标志点
在以后凡要用到一个点就x=f[x]
至关于踏入这个环就改为踏进这个超级点
可以保证\(DFS\)总复杂度为\(O(n)\)(虽然星球联盟暴力不缩点也能够过)
//并查集find int find(int x){return f[x]==x?x:f[x]=find(f[x]);} //读进来的时候就改为超级点 int x=read(),y=read();x=find(x);y=find(y); //goal为超级点 void DFS(int x,int goal) { if(lc)DFS(lc,goal); if(rc)DFS(rc,goal); if(x!=goal){f[x]=goal;siz[goal]+=siz[x];} } //每次访问点的时候都访问其find void rotate(int x) { int y=find(t[x].fa),z=find(t[y].fa); ... } void Access(int x){for(int y=0;x;y=x,x=find(t[x].fa)){splay(x);t[x].ch[1]=y;pushup(x);}} ...
V 维护原图信息
难懂,烦请细细品味
\(Access\)的目的是使得x没有实儿子,那么虚儿子即是原子树的信息
由于\(x\)的实儿子中有可能有点是原图中的儿子,那么只算虚儿子会算不全,都算会多算
以维护\(siz\)为例:
记录每一个点的\(Rs\)表示虚儿子信息,\(siz\)表示实儿子和虚儿子的信息
须要改动的地方只有\(Access\)和\(link\)
//要改变的两个操做 void Access(int x) { for(int y=0;x;y=x,x=t[x].fa) { splay(x); t[x].Rs=t[x].Rs+t[rc].siz-t[y].siz;//把一个实儿子变成虚儿子要+t[rx].siz,把一个虚儿子变成实儿子要-t[y].siz rc=y;pushup(x); } } void link(int x,int y){makeroot(x);makeroot(y);t[x].fa=y;t[y].Rs+=t[x].siz;}//link要makeroot(y)由于连上x后y到该棵splay的根都有影响
注意的是这里调用的都是\(t[son].siz\)也就是\(son\)这棵子树全部的值,而不是这个点的值!!
因为这个缘由共价大爷游长沙调试了半个小时
如何看出一道题要用\(LCT\)————动态加/删边!
只有加边操做时,维护两点是否联通请用并查集
\(findroot\)在如下题目会TLE:温暖会指引咱们前行、长跑
\[\sum_{l<=i<=r}deep(lca(i,z))\]
这是[LNOI2014]LCA的题面,方法是在这个区间内每一个点到根的路径+1,统计z到根的路径之和即为答案,处理区间时,不少时候用\[Ans(L,R)=Ans(R)-Ans(L-1)\]好比说还有这道题:2018.1.25区间子图(考试题)
Luogu LCT模板
// luogu-judger-enable-o2 //注释详尽版本 #include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<set> using namespace std; int read() { char ch=getchar(); int h=0; while(ch>'9'||ch<'0')ch=getchar(); while(ch>='0'&&ch<='9'){h=h*10+ch-'0';ch=getchar();} return h; } const int MAXN=300001; set<int>Link[MAXN]; int N,M,val[MAXN],zhan[MAXN],top=0; struct Splay{int val,sum,rev,ch[2],fa;}t[MAXN]; void Print() { for(int i=1;i<=N;i++) printf("%d:val=%d,fa=%d,lc=%d,rc=%d,sum=%d,rev=%d\n",i,t[i].val,t[i].fa,t[i].ch[0],t[i].ch[1],t[i].sum,t[i].rev); } void pushup(int x)//向上维护异或和 { t[x].sum=t[t[x].ch[0]].sum^t[t[x].ch[1]].sum^t[x].val;//异或和 } void reverse(int x)//打标记 { swap(t[x].ch[0],t[x].ch[1]); t[x].rev^=1;//标记表示已经翻转了该点的左右儿子 } void pushdown(int x)//向下传递翻转标记 { if(!t[x].rev)return; if(t[x].ch[0])reverse(t[x].ch[0]); if(t[x].ch[1])reverse(t[x].ch[1]); t[x].rev=0; } bool isroot(int x)//若是x是所在链的根返回1 { return t[t[x].fa].ch[0]!=x&&t[t[x].fa].ch[1]!=x; } void rotate(int x)//Splay向上操做 { int y=t[x].fa,z=t[y].fa; int k=t[y].ch[1]==x; if(!isroot(y))t[z].ch[t[z].ch[1]==y]=x;//Attention if() t[x].fa=z;//注意了 /* 敲黑板:这个时候y为Splay的根,把x绕上去后 x的父亲是z!表示这个splay所表示的原图中的链的链顶的父亲 这正是splay根的父亲表示的是链顶的父亲的集中体现! */ t[y].ch[k]=t[x].ch[k^1];t[t[x].ch[k^1]].fa=y; t[x].ch[k^1]=y;t[y].fa=x; pushup(y); } void splay(int x)//把x弄到根 { zhan[++top]=x; for(int pos=x;!isroot(pos);pos=t[pos].fa)zhan[++top]=t[pos].fa; while(top)pushdown(zhan[top--]); while(!isroot(x)) { int y=t[x].fa,z=t[y].fa; if(!isroot(y)) /* 这个地方和普通Splay有所不一样: 普通的是z!=goal,z不是根的爸爸 这个是y!=root,y不是根 因此实质是同样的。。。 */ (t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y); rotate(x); } pushup(x); } void Access(int x) { for(int y=0;x;y=x,x=t[x].fa){splay(x);t[x].ch[1]=y;pushup(x);} /* Explaination: 函数功能:把x到原图的同一个联通块的root弄成一条链,放在同一个Splay中 首先令x原先所在splay的最左端(x所在链的链顶)为u 那么x-u必定保留在x-root的路径中,那么直接断掉x的右儿子 而后y是上一个这么处理的链的Splay所在的根 在以前,y向x连了一条虚边(y的fa是x,x的ch不是y) 那么只要化虚为实就能够了 */ } void makeroot(int x)//函数功能:把x拎成原图的根 { Access(x);splay(x);//把x和根先弄到一块儿 reverse(x);//而后打区间翻转标记,应该在根的地方打可是找不到根因此要splay(x) /* 这里很神奇的一个区间翻转标记,那么从上往下是root-x,翻转完区间就是x-root 这样子至关于(这里打一个神奇的比喻) 一根棒子上面有一些平铺的长毛,原先是向上拉,区间翻转后就向下拉 | ↑ | ----|---- /|\ \ \|/ / ----|---- / | \ \ | / ----|---- / /|\ \ \ \|/ / ----|---- / | \ \ | / ----|---- / /|\ \ \ \|/ / ----|---- / | \ \ | / ----|---- / /|\ \ \|/ | | ↓ 哈哈哈夸我~ */ } int Findroot(int x)//函数功能:找到x所在联通块的splay的根 { Access(x);splay(x); while(t[x].ch[0])x=t[x].ch[0]; return x; } void split(int x,int y)//函数功能:把x到y的路径抠出来 { makeroot(x);//先把x弄成原图的根 Access(y);//再把y和根的路径弄成重链 splay(y);//那么就是y及其左子树存储的信息了 /* 关于这里为何要splay(y): 能够发现,makeroot后x为splay的根 可是Access以后改变了根(这就是为何凡是Access都后面跟了splay) 因此要找到根最方便就是splay,至于splayx仍是y,均可以 */ } void link(int x,int y)//函数功能:链接x,y所在的两个联通块 { makeroot(x);//把x弄成其联通块的根 t[x].fa=y;//连到y上(虚边) Link[x].insert(y);Link[y].insert(x); } void cut(int x,int y)//函数功能:割断x,y所在的两个联通块 { split(x,y); t[y].ch[0]=t[x].fa=0; Link[x].erase(y);Link[y].erase(x); /* 这里会出现一个这样的状况: 图中x和y并未直接连边,可是splay中有可能直接相连 因此必定要用set(map会慢)维护实际的连边 否则会出现莫名错误(大部分数据能够水过去,可是subtask...) */ } int main() { N=read();M=read(); for(int i=1;i<=N;i++) t[i].sum=t[i].val=read();//原图中结点编号就是Splay结点编号 for(int i=1;i<=M;i++) { int op=read(),x=read(),y=read(); if(op==0)//x到y路径异或和 { split(x,y);//抠出路径 printf("%d\n",t[y].sum); } if(op==1)//链接x,y { if(Findroot(x)^Findroot(y)) link(x,y);//x,y不在同一联通块里 } if(op==2)//割断x,y { if(Link[x].find(y)!=Link[x].end()) cut(x,y);//x,y在同一联通块 } if(op==3)//把x点的权值改为y { Access(x);//把x到根的路径设置为重链 splay(x);//把x弄到该链的根结点 t[x].val=y; pushup(x);//直接改x的val并更新 } //printf("i=%d\n",i); //Print(); } return 0; }