最近自學了一下LCT(Link-Cut-Tree),參考了Saramanda及Yang_Zhe等眾多大神的論文博客,對LCT有了一個初步的認識,LCT是一種動態樹,可以處理動態問題的算法。對於樹分治中的樹鏈剖分,只能處理靜態的數據或者在輕重鏈上的邊或點的權值,對於其他動態的處理就毫無辦法了。因此我們就需要引入LCT這個東西。那么問題來了,LCT到底是什么呢?我弄了很久總算是理解了LCT,打算總結一下LCT的基本操作。
①淺談對LCT的初步印象
LCT用來維護動態的森林,以及一些鏈上操作,是處理節點無序的有根樹組成的森林,進行一些列操作(例如合並,剪切,翻轉,更新·····)
對於一棵樹的操作,我們可以用splay維護;同理,對於一片森林,也可以用多棵splay維護,而LCT就是要把這些森林的splay聯系在一起。而LCT的核心就是access操作啦!
看完之后我們知道,LCT和靜態的樹鏈剖分很像。怎么說呢?這兩種樹形結構都是由若干條長度不等的“重鏈”和“輕邊”構成(名字可以不同,大概就是這個意思),“重鏈”之間由”輕邊”連接。就像這樣:
可以想象為一棵樹被人為的砍成了一段段。
LCT和樹鏈剖分不同的是,樹鏈剖分的鏈是不會變化的,所以可以很方便的用線段樹維護。但是,既然是動態樹,那么樹的結構形態將會發生改變,所以我們要用更加靈活的維護區間的結構來對鏈進行維護,不難想到Splay可以勝任。如何分離樹鏈也是保證時間效率的關鍵(鏈的數量和長度要平衡),樹鏈剖分的“重兒子”就體現了前人博大精深的智慧。
在這里解釋一下為什么要把樹砍成一條條的鏈:我們可以在logn的時間內維護長度為n的區間(鏈),所以這樣可以極大的提高樹上操作的時間效率。在樹鏈剖分中,我們把一條條鏈放到線段樹上維護。但是LCT中,由於樹的形態變化,所以用能夠支持合並、分離、翻轉等操作的Splay維護LCT的重鏈(注意,單獨一個節點也算是一條重鏈)。
這時我們注意到,LCT中的輕邊信息變得無法維護。為什么呢?因為Splay只維護了重鏈,沒有維護重鏈之間的輕邊;而LCT中甚至連根都可以不停的變化,所以也沒法用點權表示它父邊的邊權(父親在變化)。所以,如果在LCT中要維護邊上信息,個人認為最方便的方法應該是把邊變成一個新點和兩條邊。這樣可以把邊權的信息變成點權維護,同時為了不影響,把真正的樹上節點的點權變成0,就可以用維護點的方式維護邊。
②再談LCT的各種操作
LCT中用Splay維護鏈,這些Splay叫做“輔助樹“。輔助樹以它上面每個節點的深度為關鍵字維護,就是輔助樹中每個節點左兒子的深度小於當前節點的深度,當前節點的深度小於右兒子的深度。
可以把LCT認為是一個由Splay組成的森林,就像這樣:(三角形代表一棵Splay,對應着LCT上一條鏈)
箭頭是什么意思呢?箭頭記錄着某棵Splay對應的鏈向上由輕邊連着哪個節點,可以想象為箭頭指向“Splay 的父親”。但是,Splay的父親並不記錄有這個兒子,即箭頭是單向的。同時,每個節點要記錄它是否是它所在的Splay的根。這樣,Splay構成的森林就建成了。
這個是我的Splay節點最基本的定義:(如果要維護更多信息就像Splay維護區間那樣加上更多標記)
1 struct node{ 2 int fa,ch[2]; //父親和左右兒子。 3 bool reverse,is_root; //區間反轉標記、是否是所在Splay的根 4 }T[maxn];
LCT中基本的Splay上操作:
1 int getson(int x){ 2 return x==T[T[x].fa].ch[1]; 3 } 4 void pushreverse(int x){ 5 if(!x)return; 6 swap(T[x].ch[0],T[x].ch[1]); 7 T[x].reverse^=1; 8 } 9 void pushdown(int x){ 10 if(T[x].reverse){ 11 pushreverse(T[x].ch[0]); 12 pushreverse(T[x].ch[1]); 13 T[x].reverse=false; 14 } 15 } 16 void rotate(int x){ 17 if(T[x].is_root)return; 18 int k=getson(x),fa=T[x].fa; 19 int fafa=T[fa].fa; 20 pushdown(fa);pushdown(x); //先要下傳標記 21 T[fa].ch[k]=T[x].ch[k^1]; 22 if(T[x].ch[k^1])T[T[x].ch[k^1]].fa=fa; 23 T[x].ch[k^1]=fa; 24 T[fa].fa=x; 25 T[x].fa=fafa; 26 if(!T[fa].is_root)T[fafa].ch[fa==T[fafa].ch[1]]=x; 27 else T[x].is_root=true,T[fa].is_root=false; 28 //update(fa);update(x); //如果維護了信息,就要更新節點 29 } 30 void push(int x){ 31 if(!T[x].is_root)push(T[x].fa); 32 pushdown(x); 33 } 34 void Splay(int x){ 35 push(x); //在Splay到根之前,必須先傳完反轉標記 36 for(int fa;!T[x].is_root;rotate(x)){ 37 if(!T[fa=T[x].fa].is_root){ 38 rotate((getson(x)==getson(fa))?fa:x); 39 } 40 } 41 }
access操作:
這是LCT最核心的操作。其他所有操作都要用到它。
他的含義是”訪問某節點“。作用是:對於訪問的節點x,打通一條從樹根(真實的LCT樹)到x的重鏈;如果x往下是重鏈,那么把x往下的重邊改成輕邊。可以理解為專門開辟一條x到根的路徑,由一棵Splay維護這條路徑。
access之前:(粗的是重鏈)
access之后:
access實現的方式很簡單:先把x旋轉到所在Splay的根,然后把x的右孩子的is_root設為true(此時右孩子對應的是x下方的重鏈,這樣就斷開了x和下方的重鏈)。用y記錄上一次的x(初始化y=0),把y接到x的右孩子上,這樣就把上一次的重鏈接到了當前重鏈一起,同時記得T[y].is_root=false。記錄y=x,然后x=T[x].fa,把x上提。重復上面的步驟直到x=0。
實現代碼如下:
1 void access(int x){ 2 int y=0; 3 do{ 4 Splay(x); 5 T[T[x].ch[1]].is_root=true; 6 T[T[x].ch[1]=y].is_root=false; 7 //update(x); //如果維護了信息記得更新。 8 x=T[y=x].fa; 9 }while(x); 10 }
mroot操作:
這個操作的作用是把某個節點變成樹根(這里的根指的是整棵LCT的根)。加上access操作,就可以方便的提取出LCT上兩點之間的路徑。提取u到v的路徑只需要mroot(u),access(v),然后v所在的Splay對應的鏈就是u到v的路徑。
mroot實現的方式:
由於LCT是Splay組成的森林,所以要把x變成根就只需要讓所有Splay的父親最終指向x所在Splay。所以先access(x),Splay(x),把現在的根和將成為根的x鏈在一棵Splay中,並轉到根即可。但是我們注意到,由於x成為了新的根,所以它和原來的根所在的Splay中深度作為關鍵字的性質遭到了破壞:新根x應該是Splay中深度最小的,但是之前的操作並不會改變x的深度(也就是目前x依舊是當前Splay中深度最深的)。所以,我們需要把所在的這棵Splay翻轉過來。
(粗的是重鏈,y是原來的根)
翻轉前:
翻轉后:
這時候x才真正變成了根。
實現代碼如下:
1 void mroot(int x){ 2 access(x); 3 Splay(x); 4 pushreverse(x); 5 }
link操作:
這個操作的作用是連接兩棵LCT。對於link(u,v),表示連接u所在的LCT和v所在的LCT;
link實現的方式:
很簡單,只需要先mroot(u),然后記錄T[u].fa=v就可以了,就是把一個Splay森林連到另一個上。
實現代碼如下:
void link(int u,int v){ mroot(u); T[u].fa=v; }
cut操作:
這個操作的作用是分離出兩棵LCT。
實現代碼如下:
1 void cut(int u,int v) 2 mroot(u); //先把u變成根 3 access(v);Splay(v); //連接u、v 4 pushdown(v); //先下傳標記 5 T[u].fa=T[v].ch[0]=0; 6 //v的左孩子表示v上方相連的重鏈 7 //update(v); //記得維護信息 8 }
以上這些就是LCT的基本操作啦。
③例題分析
先來一道簡單的入門題
Time Limit: 851MS | Memory Limit: 1572864KB | 64bit IO Format: %lld & %llu |
Description
You are given a tree (an acyclic undirected connected graph) with N nodes, and edges numbered 1, 2, 3...N-1.
We will ask you to perfrom some instructions of the following form:
- CHANGE i ti : change the cost of the i-th edge to ti
or - QUERY a b : ask for the maximum edge cost on the path from node a to node b
Input
The first line of input contains an integer t, the number of test cases (t <= 20). t test cases follow.
For each test case:
- In the first line there is an integer N (N <= 10000),
- In the next N-1 lines, the i-th line describes the i-th edge: a line with three integers a b c denotes an edge between a, b of cost c (c <= 1000000),
- The next lines contain instructions "CHANGE i ti" or "QUERY a b",
- The end of each test case is signified by the string "DONE".
There is one blank line between successive tests.
Output
For each "QUERY" operation, write one integer representing its result.
Example
Input: 1 3 1 2 1 2 3 2 QUERY 1 2 CHANGE 1 3 QUERY 1 2 DONE Output: 1 3
Hint
咦,這道題不是樹鏈剖分的入門題么? 對的,不過,也可以用LCT做(樹鏈剖分只處理靜態的樹和有關輕重邊的動態更新,而LCT就是用來解決動態樹問題)
題意:
給定一棵樹,給定每條邊a,b,w (w權值)
完成兩個操作:
1.把第i條邊權值改成w
2.查詢u到v路徑上的最大權值
介紹一下access操作:查詢根到u這條路徑,並把這條路徑更新為偏愛路徑(之前的偏愛邊可能會有改變)被訪問到的u,不再有偏愛兒子。
每次access訪問,都會把這條路徑上的點用一棵splay維護,splay維護的關鍵詞是點的深度,保證左邊的子樹比當前點深度小,右邊的子樹比當前點深度大。
並且這棵樹把u作為根節點之后不再有右兒子(u沒有偏愛兒子).
而splay要維護的是一條偏愛路徑上的點。所有的splay會構成一個森林的集合。
要注意的是,在rotate判斷ff的時候,有可能ff的左右兒子都不是f,然而沒有進行判斷就會跑出神奇的錯誤。
實現代碼如下:
1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 #include<cstdlib> 5 #include<queue> 6 #include<cmath> 7 #include<algorithm> 8 using namespace std; 9 /* 10 LCT的思想類似於輕重鏈剖分 11 是用偏愛路徑划分,用splay維護一條偏愛路徑,然后像森林操作一樣搞 12 u-v邊的權值記錄在v點上 13 */ 14 #define maxn 10005 15 int n; 16 int fa[maxn]; 17 struct edge 18 { 19 int v,w; 20 edge *next; 21 edge(int _v,int _w,edge *_next) 22 { 23 v=_v; 24 w=_w; 25 next=_next; 26 } 27 }*head[maxn]; 28 struct node 29 { 30 node *f; 31 node *ch[2]; 32 bool root;//是否是所在輔助樹的根節點 33 int cost; 34 int maxcost; 35 36 }tree[maxn],*null,Tnull; 37 void init(node *u) 38 { 39 u->f=u->ch[0]=u->ch[1]=null; 40 u->root=true; 41 u->cost=u->maxcost=0; 42 } 43 void pushup(node *p) 44 { 45 p->maxcost=max(max(p->ch[0]->maxcost,p->ch[1]->maxcost),p->cost); 46 } 47 void rotate(node *u) 48 { 49 node *f=u->f; 50 node *ff=f->f; 51 int d=u==f->ch[1]; 52 53 f->ch[d]=u->ch[d^1]; 54 if(u->ch[d^1]!=null)u->ch[d^1]->f=f; 55 56 u->f=ff; 57 if(ff!=null) 58 { 59 if(ff->ch[0] == f) 60 { 61 ff->ch[0] = u; 62 } 63 else if(ff->ch[1] == f)//一定要用Else If,如果直接用Else會出現錯誤,因為樹本身可能不是二叉樹,雖然生成的Splay Tree是 64 { 65 ff->ch[1] = u; 66 } 67 } 68 69 u->ch[d^1]=f; 70 f->f=u; 71 72 pushup(f); 73 pushup(u); 74 if(f->root) 75 { 76 u->root=true; 77 f->root=false; 78 } 79 } 80 void splay(node *u) 81 { 82 if(u==null)return ; 83 while(!u->root) 84 { 85 node *f=u->f; 86 if(f->root) 87 { 88 rotate(u); 89 } 90 else 91 { 92 node *ff=f->f; 93 int d=u==f->ch[1]; 94 int dd=f==ff->ch[1]; 95 if(d==dd)rotate(f); 96 else rotate(u); 97 rotate(u); 98 } 99 } 100 pushup(u); 101 } 102 vector<pair<int,int> >E; 103 char ss[20]; 104 void access(node *u,bool flag) 105 { 106 node *v=null; 107 while(u!=null) 108 { 109 splay(u); 110 if(flag) 111 { 112 if(u->f==null) 113 { 114 printf("%d\n",max(u->ch[1]->maxcost,v->maxcost)); 115 } 116 } 117 u->ch[1]->root=true; 118 u->ch[1]=v; 119 v->root=false; 120 pushup(u); 121 v=u; 122 u=u->f; 123 } 124 } 125 void query(int u,int v) 126 { 127 access(tree+u,0); 128 access(tree+v,1); 129 } 130 void change(int u,int w) 131 { 132 access(tree+u,0); 133 splay(tree+u); 134 tree[u].cost=w; 135 pushup(tree+u); 136 } 137 void bfs() 138 { 139 memset(fa,-1,sizeof(fa)); 140 queue<int >q; 141 q.push(1); 142 fa[1]=0; 143 while(!q.empty()) 144 { 145 int u=q.front(); 146 q.pop(); 147 for(edge *i=head[u];i;i=i->next) 148 { 149 if(fa[i->v]==-1) 150 { 151 fa[i->v]=u; 152 tree[i->v].f=tree+u; 153 tree[i->v].cost=tree[i->v].maxcost=i->w; 154 q.push(i->v); 155 } 156 } 157 } 158 } 159 int main() 160 { 161 int T; 162 scanf("%d",&T); 163 null=&Tnull; 164 init(null); 165 while(T--) 166 { 167 scanf("%d",&n); 168 for(int i=1;i<=n;i++) 169 { 170 head[i]=NULL; 171 init(&tree[i]); 172 } 173 E.clear(); 174 for(int i=1;i<n;i++) 175 { 176 int a,b,c; 177 scanf("%d%d%d",&a,&b,&c); 178 head[a]=new edge(b,c,head[a]); 179 head[b]=new edge(a,c,head[b]); 180 E.push_back(make_pair(a,b)); 181 } 182 bfs(); 183 while(scanf("%s",ss)!=EOF) 184 { 185 if(ss[0]=='D')break; 186 if(ss[0]=='Q') 187 { 188 int u,v; 189 scanf("%d%d",&u,&v); 190 query(u,v); 191 } 192 else if(ss[0]=='C') 193 { 194 int a,w; 195 scanf("%d%d",&a,&w); 196 a--; 197 int u=E[a].first; 198 int v=E[a].second; 199 if(fa[u]==v) 200 { 201 change(u,w); 202 } 203 if(fa[v]==u) 204 { 205 change(v,w); 206 } 207 } 208 } 209 } 210 return 0; 211 }
一道入門題怎么夠呢?那就再來一道操作多一點的~~~
Query on The Trees
Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 65768/65768 K (Java/Others)
Total Submission(s): 6250 Accepted Submission(s): 2504
There are N nodes, each node will have a unique weight Wi. We will have four kinds of operations on it and you should solve them efficiently. Wish you have fun!
For each case, the first line contains only one integer N.(1 ≤ N ≤ 300000) The next N‐1 lines each contains two integers x, y which means there is an edge between them. It also means we will give you one tree initially.
The next line will contains N integers which means the weight Wi of each node. (0 ≤ Wi ≤ 3000)
The next line will contains an integer Q. (1 ≤ Q ≤ 300000) The next Q lines will start with an integer 1, 2, 3 or 4 means the kind of this operation.
1. Given two integer x, y, you should make a new edge between these two node x and y. So after this operation, two trees will be connected to a new one.
2. Given two integer x, y, you should find the tree in the tree set who contain node x, and you should make the node x be the root of this tree, and then you should cut the edge between node y and its parent. So after this operation, a tree will be separate into two parts.
3. Given three integer w, x, y, for the x, y and all nodes between the path from x to y, you should increase their weight by w.
4. Given two integer x, y, you should check the node weights on the path between x and y, and you should output the maximum weight on it.
You should output a blank line after each test case.
5
1 2
2 4
2 5
1 3
1 2 3 4 5
6
4 2 3
2 1 2
4 2 3
1 3 5
3 2 1 4
4 1 4
題意:
給定一棵樹,維護4個操作:
1.給定u,v,如果不在一棵樹上,它們之間連邊。
2.給定u,v如果u!=v且在同一棵樹上,把u設為整棵樹的根,再把v和它的父親斷開。
3.給定u,v,w,如果u,v在同一條樹上,把他們之間的點的權值+w
4.給定u,v,如果u,v在同一棵樹上,求出他們路徑上的最大值。
違法操作輸出-1
思路:
連邊操作,訪問u,把u旋到根,把它左右翻轉即可(翻轉前splay上u一定沒有右節點(沒有偏愛兒子),翻轉后一定沒有左兒子,說明它就是從根到u路徑上深度最小的了,就成為了根),把它接到v上,設為整棵樹的根:一樣的先access(u),splay(u)再rev(u)。
斷開:access(u),splay(u),但不翻轉,這樣u沒有右兒子但有左兒子,和左子樹斷開即可。
對於3、4操作,可以先把u設為根,再access(v)然后找到u-v這條路徑上splay的根節點在哪里,信息都記錄在那個節點上!
實現代碼如下:
1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 #include<cstdlib> 5 #include<algorithm> 6 #include<queue> 7 #include<cmath> 8 using namespace std; 9 const int maxn=300000+20; 10 const int inf=0x3f3f3f3f; 11 struct node 12 { 13 node *f; 14 node *ch[2]; 15 bool rev; 16 int add; 17 int mm; 18 int key; 19 }tree[maxn],*null,*cur; 20 void init() 21 { 22 null=tree; 23 null->f=null->ch[0]=null->ch[1]=null; 24 null->rev=0; 25 null->add=0; 26 null->mm=null->key=-inf; 27 cur=tree+1; 28 } 29 node *newnode(int key) 30 { 31 cur->f=cur->ch[0]=cur->ch[1]=null; 32 cur->mm=cur->key=key; 33 cur->add=0; 34 cur->rev=0; 35 return cur++; 36 } 37 bool isroot(node *x) 38 { 39 return x==null||x->f->ch[0]!=x&&x->f->ch[1]!=x; 40 } 41 void pushup(node *u) 42 { 43 u->mm=max(u->key,max(u->ch[0]->mm,u->ch[1]->mm)); 44 } 45 void pushdown(node *u) 46 { 47 if(u==null)return ; 48 if(u->rev) 49 { 50 swap(u->ch[0],u->ch[1]); 51 if(u->ch[0]!=null)u->ch[0]->rev^=1; 52 if(u->ch[1]!=null)u->ch[1]->rev^=1; 53 u->rev=0; 54 } 55 if(u->add) 56 { 57 if(u->ch[0]!=null) 58 { 59 u->ch[0]->add+=u->add; 60 u->ch[0]->mm+=u->add; 61 u->ch[0]->key+=u->add; 62 } 63 if(u->ch[1]!=null) 64 { 65 u->ch[1]->add+=u->add; 66 u->ch[1]->mm+=u->add; 67 u->ch[1]->key+=u->add; 68 } 69 u->add=0; 70 } 71 } 72 void rotate(node *u) 73 { 74 node *f=u->f; 75 node *ff=f->f; 76 int d=u==f->ch[1]; 77 78 if(u->ch[d^1]!=null)u->ch[d^1]->f=f; 79 f->ch[d]=u->ch[d^1]; 80 81 u->f=ff; 82 if(ff!=null) 83 { 84 if(f==ff->ch[0])ff->ch[0]=u; 85 else if(f==ff->ch[1])ff->ch[1]=u; 86 } 87 88 u->ch[d^1]=f; 89 f->f=u; 90 91 pushup(f); 92 pushup(u); 93 } 94 node *sta[maxn]; 95 int cnt; 96 void splay(node *u) 97 { 98 if(u==null)return ; 99 cnt=1; 100 sta[0]=u; 101 for(node *y=u;!isroot(y);y=y->f) 102 { 103 sta[cnt++]=y->f; 104 } 105 while(cnt)pushdown(sta[--cnt]); 106 while(!isroot(u)) 107 { 108 node *f=u->f; 109 node *ff=f->f; 110 if(isroot(f)) 111 { 112 rotate(u); 113 } 114 else 115 { 116 int d=u==f->ch[1]; 117 int dd=f==ff->ch[1]; 118 if(d==dd)rotate(f); 119 else rotate(u); 120 rotate(u); 121 } 122 } 123 pushup(u); 124 } 125 node *access(node *u) 126 { 127 node *v=null; 128 while(u!=null) 129 { 130 splay(u); 131 v->f=u; 132 u->ch[1]=v; 133 pushup(u); 134 v=u; 135 u=u->f; 136 } 137 return v; 138 } 139 void link(node *u,node *v) 140 { 141 access(u); 142 splay(u); 143 u->rev=1; 144 u->f=v; 145 } 146 bool sam(node *u,node *v) 147 { 148 while(u->f!=null)u=u->f; 149 while(v->f!=null)v=v->f; 150 return u==v; 151 } 152 void changeroot(node *u) 153 { 154 access(u)->rev^=1; 155 } 156 void cut(node *u) 157 { 158 access(u); 159 splay(u);//access+旋轉之后左子樹的點都比u小,一定會有u的父親 160 u->ch[0]=u->ch[0]->f=null; 161 pushup(u); 162 } 163 node *getroot(node *u) 164 { 165 access(u); 166 splay(u); 167 while(u->f!=null)u=u->f; 168 splay(u); 169 return u; 170 } 171 int n,m; 172 int det[maxn]; 173 struct edge 174 { 175 int u,v; 176 }E[maxn]; 177 int main() 178 { 179 while(scanf("%d",&n)!=EOF) 180 { 181 init(); 182 for(int i=1;i<n;i++) 183 { 184 scanf("%d%d",&E[i].u,&E[i].v); 185 } 186 for(int i=1;i<=n;i++) 187 { 188 scanf("%d",&det[i]); 189 newnode(det[i]); 190 } 191 for(int i=1;i<n;i++) 192 { 193 link(tree+E[i].u,tree+E[i].v); 194 } 195 scanf("%d",&m); 196 for(int i=1;i<=m;i++) 197 { 198 int k,a,b; 199 scanf("%d",&k); 200 if(k==1) 201 { 202 scanf("%d%d",&a,&b); 203 if(sam(tree+a,tree+b))printf("-1\n"); 204 else 205 { 206 link(tree+a,tree+b); 207 } 208 } 209 else if(k==2) 210 { 211 scanf("%d%d",&a,&b); 212 if(a==b||!sam(tree+a,tree+b))printf("-1\n"); 213 else 214 { 215 changeroot(tree+a); 216 cut(tree+b); 217 } 218 } 219 else if(k==3) 220 { 221 int w; 222 scanf("%d%d%d",&w,&a,&b); 223 if(!sam(tree+a,tree+b))printf("-1\n"); 224 else 225 { 226 changeroot(tree+a); 227 access(tree+b); 228 node *q=getroot(tree+b); 229 q->add+=w; 230 q->mm+=w; 231 q->key+=w; 232 } 233 } 234 else if(k==4) 235 { 236 scanf("%d%d",&a,&b); 237 if(!sam(tree+a,tree+b))printf("-1\n"); 238 else 239 { 240 changeroot(tree+a); 241 access(tree+b); 242 node *q=getroot(tree+b); 243 printf("%d\n",q->mm); 244 } 245 } 246 } 247 printf("\n"); 248 } 249 return 0; 250 }
④習題推薦
我推薦幾個LCT的練習題練習下上述的知識點吧!
- BZOJ 2049 SDOI2008洞穴勘探,模板題,只需要link和cut,然后詢問連通性。
-
BZOJ 2002 HNOI2010彈飛綿羊,模板題,需要link和詢問某點到根的路徑長度。
-
BZOJ 3669 NOI2014魔法森林,LCT的綜合應用。
相信做完上述例題,你也能對LCT有了個初步的認識吧,加油!
⑤參考資料