關於樹論【動態樹問題(LCT)】
搬運:看一道caioj1439
題目描述
一開始給你一棵n個點n-1條邊的樹,每個點有一個權值wi。
三種操作:
op=1 u v :在點u和點v之間建一條邊。
op=2 u v:摧毀點u到點v之間的邊。
op=3 w u v:將點u和點v之間路徑上的點(包括u,v),權值增加w。
op=4 u v:詢問點u到點v之間路徑上的點(包括u,v),權值最大值。
當操作違法時(詢問一中u,v已經相連,二三四中u,v不聯通,一二操作u==v)不進行操作並輸出-1。
輸入
從文件weight.in中讀取輸入。
第1行為1個正整數n,表示點的個數。
第2~n行為開始樹所有的邊,每行兩個正整數u,v,代表u和v之間有一條邊。
第n+1行有n個正整數,表示一開始點的權值。
下一行為一個正整數m代表下來有m個操作。
以下m行,一行表示一個操作。
行首先輸入一個正整數op。
當op=1,2,4時,輸入兩個正整數u,v
當op=3時,輸入三個正整數w,u,v
操作如題意
輸出
輸出到文件weight.out。
對每個4操作,輸出點u到點v之間路徑上的點(包括u,v),權值最大值。同時對於違法情況輸出-1。
該類動態樹問題一個突出點就是動態,假如沒有1、2操作當然可以方便的運用樹鏈剖分算法水過(詳見第8章 樹鏈剖分)。前一章的伸展樹只支持改變樹的形態,難以對樹的結構進行改變,對於建邊刪邊的操作的需要,我們要運用多棵伸展樹組成新樹,即解決該類動態樹問題的普遍方法,Link-Cut-Tree,俗稱LCT。
它跟樹鏈剖分類似,只不過樹剖用線段樹維護重鏈,而LCT用伸展樹(是不是很高大上),在兩棵伸展樹之間,如果它們屬於同一個LCT,那麽將有一條虛邊,連接著它們,在不影響伸展樹的正常操作前提上,保持應有的連系。
大家可以感性的認識….可以假設一開始問題給出的樹邊都是虛邊,我們人為的在上面畫重鏈,每條重鏈用一棵伸展樹維護他(就跟線段樹一個道理嘛,目標是減少暴力枚舉的時間,只不過伸展樹更加快捷靈活),關鍵的,如果沒有連邊刪邊操作,同伸展樹一樣,整棵樹的結構是不變的。
當然啦,題目也可能給出很多棵樹,我們可以臆想一下,這些樹都屬於0節點的子樹,只不過他們連的邊被“操作刪除”了,這樣也是合理的。同樣道理,當我們在解決動態樹問題的過程中,有時也會出現這棵樹被分成多份。也就是說,Link-Cut-Tree本質上這個圖可以是一個森林。
講講操作吧。
最重要的access(x):令x到當前所處的樹的根這條路徑成為偏愛路徑(相當於樹剖的重鏈),然後用splay維護,這是與樹剖最大的不同,這樣的靈活性也符合動態樹。
make_root(x):令x成為當前樹的根,但是!!不是在當前重鏈中伸展樹的根,也不是整個圖中所有點的根,而是,x當前所處的樹的根! 由於 LCT的Link和Cut操作,註定了整個圖可能出現多棵樹,樹與樹之間如果不添加邊,都是一個獨立的動態樹。
Link(x,y):讓x成為根,然後連一條虛邊到y就OK了。
Cut(x,y):先將x設為根(假設現在是點1)假設y是點6,那我們將1~6的路徑設為偏愛路徑(放在一棵伸展樹裏)將6旋轉到伸展樹的根,可以發現,點1肯定在伸展樹的最左端,讓y斷開與左端的連接就行了。
findroot(x):同理,真正在樹中的根肯定在樹的最左邊,所以說找根其實很簡單。
PS:所以在make_root後要讓整棵伸展樹翻轉,比如說將6變為根,1,4都在它左邊,這樣就不科學了。
如果還有不明白的,可以看caioj的書,還有上caioj1439看視頻,視頻非常好!!出視頻的人改變了我的一生,從未見過有如此懂我的人,他太強了,我崇拜他一輩子!!
#include<cstdio> #include<iostream> #include<cstring> #include<algorithm> #include<cmath> using namespace std; struct node { int f,d,c,n,son[2],mx,ad; bool fz; }tr[310000]; void add(int x) { tr[x].d+=tr[x].ad;tr[x].mx+=tr[x].ad; int lc=tr[x].son[0],rc=tr[x].son[1]; tr[lc].ad+=tr[x].ad; tr[rc].ad+=tr[x].ad; tr[x].ad=0; } void update(int x) { int lc=tr[x].son[0],rc=tr[x].son[1]; tr[x].c=tr[lc].c+tr[rc].c+tr[x].n; if(tr[lc].ad!=0)add(lc); if(tr[rc].ad!=0)add(rc); if(lc==0)tr[lc].mx=0; if(rc==0)tr[rc].mx=0; tr[x].mx=max(max(tr[lc].mx,tr[rc].mx),tr[x].d); } void reverse(int x) { tr[x].fz=false; swap(tr[x].son[0],tr[x].son[1]); int lc=tr[x].son[0],rc=tr[x].son[1]; tr[lc].fz=1-tr[lc].fz; tr[rc].fz=1-tr[rc].fz; } void rotate(int x,int w) { int f=tr[x].f,ff=tr[f].f; int R,r; R=f;r=tr[x].son[w]; tr[R].son[1-w]=r; if(r!=0)tr[r].f=R; R=ff;r=x; if(tr[R].son[0]==f)tr[R].son[0]=r; else if(tr[R].son[1]==f)tr[R].son[1]=r; tr[r].f=R; R=x;r=f; tr[R].son[w]=r; tr[r].f=R; update(f); update(x); } int tmp[310000]; void splay(int x,int rt) { int s=0,i=x; while(tr[i].f!=0&&(tr[tr[i].f].son[0]==i||tr[tr[i].f].son[1]==i)) { tmp[++s]=i; i=tr[i].f; } tmp[++s]=i; while(s!=0) { i=tmp[s];s--; if(tr[i].fz==true)reverse(i); if(tr[i].ad!=0)add(i); } while(tr[x].f!=rt&&(tr[tr[x].f].son[0]==x||tr[tr[x].f].son[1]==x))//還有虛邊啊! { int f=tr[x].f,ff=tr[f].f; if(ff==rt||(tr[ff].son[0]!=f&&tr[ff].son[1]!=f)) { if(x==tr[f].son[0])rotate(x,1); else rotate(x,0); } else { if(tr[f].son[0]==x&&tr[ff].son[0]==f){rotate(f,1);rotate(x,1);} else if(tr[f].son[1]==x&&tr[ff].son[0]==f){rotate(x,0);rotate(x,1);} else if(tr[f].son[0]==x&&tr[ff].son[1]==f){rotate(x,1);rotate(x,0);} else if(tr[f].son[1]==x&&tr[ff].son[1]==f){rotate(f,0);rotate(x,0);} } } } int n,w[310000]; void make_tree() { for(int i=0;i<=n;i++) { tr[i].f=0; tr[i].mx=tr[i].d=w[i]; tr[i].c=1;tr[i].n=1; tr[i].son[0]=tr[i].son[1]=0; tr[i].fz=false;tr[i].ad=0; } } void access(int x)//訪問x //還記得樹剖的重兒子嗎?這是令點x到整棵動態樹的根這條路徑變成偏愛路徑(相當於樹剖的重鏈),這一條路徑就是一棵伸展樹。 { int y=0; while(x!=0) { splay(x,0); tr[x].son[1]=y; if(y!=0)tr[y].f=x; y=x;x=tr[x].f; } } void makeroot(int x)//讓x成為當前樹的根 { access(x);splay(x,0);//因為是鏈,splay之後只有左孩子(上面y=0) tr[x].fz=1-tr[x].fz;//因為要讓x成為整棵樹的根,所以x的深度要最小(通過翻轉實現),為find_root做準備 } void link(int x,int y) {//為什麽可以直接用makeroot??因為判斷過x和y的find_root 是否相同,不相同表示x和y是不聯通的 makeroot(x);tr[x].f=y;access(x);//刪去access是沒有影響的,但從定義上說應該加上 } void cut(int x,int y) { makeroot(x); access(y);splay(y,0); tr[tr[y].son[0]].f=0;tr[y].son[0]=0; update(y); } int find_root(int x)//訪問完x後,x所屬的伸展樹的最左端的點就是所在樹真正的根,因為伸展樹實際意義上就是一條鏈啊!! { access(x);splay(x,0); while(tr[x].son[0]!=0)x=tr[x].son[0]; return x; } void increase(int x,int y,int W)//令x,y處於一棵伸展樹,y為根,由於是鏈,直接更新y的ad就行了 { makeroot(x); access(y);splay(y,0); tr[y].ad+=W; } int findmax(int x,int y)//同理,這也是一樣的 { makeroot(x); access(y);splay(y,0); update(y);return tr[y].mx; } struct edge { int x,y; }e[310000]; int main() { freopen("weight.in","r",stdin); freopen("weight.out","w",stdout); int m,op,x,y,W; while(scanf("%d",&n)!=EOF) { for(int i=1;i<n;i++)scanf("%d%d",&e[i].x,&e[i].y); for(int i=1;i<=n;i++)scanf("%d",&w[i]); make_tree(); for(int i=1;i<n;i++) link(e[i].x,e[i].y); scanf("%d",&m); for(int i=1;i<=m;i++) { scanf("%d",&op); if(op==1) { scanf("%d%d",&x,&y); if(find_root(x)==find_root(y)||x==y) printf("-1\n"); else link(x,y); } else if(op==2) { scanf("%d%d",&x,&y); if(find_root(x)!=find_root(y)||x==y) printf("-1\n"); else cut(x,y); } else if(op==3) { scanf("%d%d%d",&W,&x,&y); if(find_root(x)!=find_root(y)) printf("-1\n"); else increase(x,y,W); } else { scanf("%d%d",&x,&y); if(find_root(x)!=find_root(y)) printf("-1\n"); else printf("%d\n",findmax(x,y)); } } printf("\n"); } return 0; }
關於樹論【動態樹問題(LCT)】