「學習筆記」LCT重學
其實理論上算是第三次學了,寒假的時候就等於沒學
定義
起源是有些樹要動態加邊或者刪邊,所以這時候用到了 \(lct\)
我們把 \(splay\) 放到外層作為外層樹,每個點被包含在一個單獨的 \(splay\) 中
其實這裡主要是運用了 \(splay\) 可以區間反轉的功能(當然可以 \(fhq\) 但是複雜度多一個 \(\log\))
同時在 \(splay\) 裡面中序遍歷得到的點的深度是遞增的,也就是說不一定在當前 \(splay\) 的根就是這個子樹裡面深度最淺的點
因此我們把邊就得拆成實邊和虛邊,這裡在同一個 \(splay\) 裡面的邊都是實邊,而不在裡面的邊則是虛邊
這裡注意:父子連邊不變,不是說當前 \(splay\)
那麼就有一些定義
\(fa[x]:\) \(x\) 在 \(splay\) 裡面的父親,不是原樹
\(ls[x],rs[x]\) 在 \(splay\) 裡面的左右兒子,真樹裡面的連邊都是靠 \(fa[son]=x\) 來的
操作
access
聯通根到當前點的鏈
這個每個 \(splay\) 跟著做就行了,每次把當前點幹到 \(splay\) 的根,然後改兒子
這裡是把 \(rs[fat]=now\) ,根據深度的原則不難得到
所以簡單的程式碼如下:
inline void access(int x){ for(reg int y=0;x;x=fa[y=x]) splay(x),rs[x]=y,push_up(x); return ; }
其實這裡是換了個更的方式:把更新兒子的步驟放到了上面,這裡被覆蓋的兒子的父親沒變,但是父子關係變成了虛邊
makeroot
指定根
很好說,直接打通鏈時候翻上去就行了,但是很坑的是這樣的話沒有深度保證
所以要翻轉整個當前 \(splay\),打標記即可,和普通 \(splay\) 沒有區別
inline void pushroot(int x){ swap(ls[x],rs[x]); fl[x]^=1; return ; } inline void makeroot(int x){ access(x); splay(x); pushroot(x); return ; }
findroot
找到原樹上的根
換到根之就找左兒子,也就是:
inline int findroot(int x){
access(x); splay(x);
while(ls[x]) x=ls[x];
return splay(x),x;//多splay來保證複雜度……
}
當然這樣寫是非常慢的,那麼特定場景下可以用並查集來進行替換和卡常
split
打通一條鏈,隨便欽定一個點為根然後連上就行了
inline void split(int x,int y){
makeroot(x); access(y); splay(y);
return ;
}
然後直接查詢 \(s[y]\) 就是這個路徑上面的資訊,需要理解一下為什麼這樣就能維護出來單獨的路徑
link
連線兩個點之間的邊
inline void link(int x,int y){
make_root(x); if(findroot(y)!=x) fa[x]=y;
return ;
}
cut
斷開邊,先把一個點轉到必然是另一個的父親然後判斷合法性
也就是說:
inline void cut(int x,int y){
make_root(x);
if(findroot(y)!=x||fa[y]!=x||ls[y]) return ;
//第一個是不在一個樹上,findroot(y) 之後 x 是這個splay的根
//如果y的父親不是x那麼必然沒有連邊,考慮findroot中的更改
//如果有ls[y] 那麼就有越級父親
rs[x]=0; fa[y]=0; push_up(x);
return ;
}
這裡寫 \(lct\) 的 \(splay\) 和一般的不太一樣,具體如下:
\((1)\) 維護一個是不是當前splay的根的函式:
inline bool isroot(int x){return ls[fa[x]]!=x&&rs[fa[x]]!=x;}
\((2)\) \(rotate\) 和 \(splay\) 的時候記得判斷 \(z=fa[fa[x]]\) 的情況,不能找到另一個splay上面
\((3)\) 下方 \(makeroot\) 標記的時候要注意從上往下,原因可以手玩一下
這樣的話板子就隨便打了
功能
維護鏈上資訊
其實本質上就是 \(split\) 一下,然後有各種打標記的方式
Luogu 1501
裸題,直接打標記就行了
Luogu 4332
顯然改變一個 \([n+1,3n]\) 的點的本質會修改一條鏈上的答案
肯定是臨界的會改,問題轉化成了維護鏈上最深的 \(cnt[x]\) 不是 \(1/2\) 的點的位置
打通一條鏈的操作就是 \(access\) ,然後在平衡樹上二分
具體而言就是 \(push\_up\) 維護幾個子樹面是不是都是 \(1/2\) 即可
時間複雜度 \(O(n\log^2 n)\)
貌似有一個少 \(\log\) 的寫法,就是記錄子樹裡面的最深 \(val\neq 1/2\) 的點
如果修改值的話需要交換
這題寫的原則就是多 $push_up $
維護雙聯通分量/聯通性
Luogu2542
逆序之後考慮如何維護必經邊
這個必經邊容易讓人想到點雙,那麼考慮縮點
每次如果 \(link\) 失敗了就刪掉環上的點,縮成一個新點,所有連的點都指向這個點
但是需要更改的是 \(access\) 的時候是要跳 \(find(fa[x])\) 的
維護生成樹或者樹邊資訊
邊權很難維護,如果按照一些樹題放到兒子上的話一變父子關係就廢掉了
所以考慮拆點,把邊權放到一個新的點上,因為比較優秀的編號方式,每個新點的 \(id>n\)
所以更改或者一些其他操作就好說了
link(e[i].id,e[i].x); link(e[i].id,e[i].y);
cur(e[i].id,e[i].x); cut(e[i].id,e[i].y);
NOI2014 魔法森林
看到是多維的先想降維,所以 \(sort\) 掉 \(a/b\)
那麼然後的問題是如何維護一個用 \(a\) 最小的生成樹
那麼每次加邊如果成環就斷掉環上最大的 \(a\) 然後更新答案
為啥原來那麼菜還要去抄題解呀
維護虛子樹資訊
記 \(s_i\) 表示虛子樹資訊,那麼不一樣的地方首先是 \(access\)
for(reg int y=x;x;x=fa[y=x]){
splay(x);
s[x]-=s[rs[x]],si[x]+=rs[x];
s[x]+=s[y],si[x]-=s[y];
rs[x]=y;
}
然後 \(link\) 的時候要先把 \(y\) 整到根上面,防止祖先的資訊被記漏
inline void link(int x,int y){
make_root(x); if(findroot(y)==x) return ;
fa[x]=y; access(y); splay(y); si[y]+=s[x];
push_up(y); //真的別忘記了多push_up
return ;
}
BJOI2014 大融合
貌似線段樹合併隨便做?
如果 \(lct\) 的話考慮如果 \(split(x,y)\) 之後那麼有剩下的邊就是虛的了
那麼維護上 \(s[x],si[x]\) 查詢的時候回答 \(s[x]\times(s[y]-s[x])\)
維護樹上染色聯通塊
SP16549
正在做