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; }