Link-Cut-Tree
Tags:數據結構
更好閱讀體驗:https://www.zybuluo.com/xzyxzy/note/1027479
一、概述
\(LCT\),動態樹的一種,又可以\(link\)又可以\(cut\)
引用:http://www.cnblogs.com/zhoushuyu/p/8137553.html
二、題目
初步
- [x] P2147 [SDOI2008]Cave 洞穴勘測 https://www.luogu.org/problemnew/show/P2147
- [x] P3690 【模板】Link Cut Tree https://www.luogu.org/problemnew/show/P3690
- [x] P3203 [HNOI2010]彈飛綿羊 https://www.luogu.org/problemnew/show/P3203
- [x] P2173 [ZJOI2012]網絡 https://www.luogu.org/problemnew/show/P2173
- [x] P1501 [國家集訓隊]Tree II https://www.luogu.org/problemnew/show/P1501
- [x] P4172 [WC2006]水管局長 https://www.luogu.org/problemnew/show/P4172
- [x] P2387 [NOI2014]魔法森林 https://www.luogu.org/problemnew/show/P2387
- [x] [HDU5398] GCD Tree https://vjudge.net/problem/HDU-5398
- [x] [BZOJ4736]溫暖會指引我們前行 http://uoj.ac/problem/274
- [x] P1505 [國家集訓隊]旅游 https://www.luogu.org/problemnew/show/P1505
- [x] P2542 [AHOI2005]航線規划 https://www.luogu.org/problemnew/show/P2542
- [x] P2486 [SDOI2011]染色 https://www.luogu.org/problemnew/show/P2486
- [x] P4180 [Beijing2010組隊]次小生成樹Tree https://www.luogu.org/problemnew/show/P4180
進階
- [ ] [Luogu/POJ3522]最小差值生成樹 https://www.luogu.org/problemnew/show/P4234
- [x] [BZOJ4998]星球聯盟 http://www.lydsy.com/JudgeOnline/problem.php?id=4998
- [x] [BZOJ2959]長跑 http://www.lydsy.com/JudgeOnline/problem.php?id=2959
- [x] [BJOI2014]大融合 https://loj.ac/problem/2230
- [x] [UOJ207]共價大爺游長沙 http://uoj.ac/problem/207
- [x] [COGS2701]動態樹 http://cogs.pro:8080/cogs/problem/problem.php?pid=2701
- [x] [BZOJ3626][LNOI2014]LCA https://ruanx.pw/bzojch/p/3626.html
- [ ] [THUWC 2017]在美妙的數學王國中暢游 https://loj.ac/problem/2289
- [ ] 1.19樹
- [ ] 1.22Wander
變態
- [ ] P3721 [AH2017/HNOI2017]單旋 https://www.luogu.org/problemnew/show/P3721
- [ ] [BZOJ3514]Codechef MARCH14 GERALD07加強版 https://ruanx.pw/bzojch/p/3514.html
- [ ] P3703 [SDOI2017]樹點塗色 https://www.luogu.org/problemnew/show/P3703
- [ ] P3613 睡覺困難綜合征 https://www.luogu.org/problemnew/show/P3613
三、支持操作
I 維護聯通性
維護兩點聯通性,較易,例題:Cave 洞穴勘測
II 維護樹鏈信息
正是由於這個LCT可以代替樹鏈剖分的關於鏈的操作(關於子樹信息是無法做到的,感謝@cjfdf斧正「2018.2.25」)
運用\(split\)操作把\(x\)到\(y\)這條鏈摳出來操作
例題:【模板】Link Cut Tree
這是\(LCT\)的最大作用之一,幾乎在每道題中都有體現
PS:樹剖的常數小且相對容易調試,建議能寫樹剖則寫(如“初步”的后三題,沒有刪邊操作)
III 維護生成樹
例題:“初步”中水管局長到溫暖會指引我們前行
這里較為重要,理解需要時間
引入:一條路徑的權值定義為該路徑上所有邊的邊權最大值,問x到y的所有路徑中,路徑權值最小的路徑的權值是多少,要求支持加邊或刪邊,\(O(nlogn)\)求解
解決:
- 要求支持加邊,那么每構成一個環就把環內最大邊刪掉,若支持刪邊則離線逆序處理
- 化邊為點,每個\(splay\)節點記錄\({fa,ch[2],rev,val,id,d1,d2}\),分別表示父親,孩子,翻轉標記,該點權值(如果該點為邊則為邊權,如果為點那么最大生成樹中值為\(inf\),最小生成樹中值為\(-inf\)),在該節點所在的\(splay\)中、以該節點為根的子樹中權值最大(小)的點的編號,(若該節點表示邊)與該邊相連的兩個點的編號
- 加入一條邊\((x,y)\)的時候,判斷\(x,y\)是否聯通,若聯通,\(split(x,y)\),判斷這條路徑上的邊權最大值(最小值)和所加入的邊的邊權的關系,再決定\(continue\)或\(cut\)再\(link\)
pushup片段
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 維護原圖信息
例題:大融合、動態樹
難懂,煩請細細品味
解釋:
- 先知道這幾個名詞和性質:
- A、實兒子:\(x\)在\(splay\)中的兒子
- B、虛兒子:與\(x\)在原圖中有直接連邊但和\(x\)不在同一棵\(splay\)中
- C、若在原圖中\(x\)是\(y\)的父親,且\(x\),\(y\)不在同一棵\(splay\)中,那么\(y\)所在的\(splay\)的根的父親指向\(x\)
- 再知道這幾個要點:
- A、\(x\)與其實兒子在原圖中不一定有直接連邊
- B、上文講到的維護樹鏈的信息都是維護實兒子的信息
- C、\(x\)的實兒子信息包括了實兒子的虛兒子和實兒子的實兒子
- 那么在原圖中的子樹信息就可以這樣求:Access(x)后返回x虛兒子的信息
實現
\(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\)這棵子樹所有的值,而不是這個點的值!!
由於這個原因共價大爺游長沙調試了半個小時
四、做題經驗
1、辨別
如何看出一道題要用\(LCT\)————動態加/刪邊!
2、常數
只有加邊操作時,維護兩點是否聯通請用並查集
\(findroot\)在以下題目會TLE:溫暖會指引我們前行、長跑
3、所謂奇技淫巧
這是[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;
}