1. 程式人生 > 實用技巧 >「學習筆記」LCT重學

「學習筆記」LCT重學

其實理論上算是第三次學了,寒假的時候就等於沒學


定義

起源是有些樹要動態加邊或者刪邊,所以這時候用到了 \(lct\)

我們把 \(splay\) 放到外層作為外層樹,每個點被包含在一個單獨的 \(splay\)

其實這裡主要是運用了 \(splay\) 可以區間反轉的功能(當然可以 \(fhq\) 但是複雜度多一個 \(\log\)

同時在 \(splay\) 裡面中序遍歷得到的點的深度是遞增的,也就是說不一定在當前 \(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]\) 就是這個路徑上面的資訊,需要理解一下為什麼這樣就能維護出來單獨的路徑

連線兩個點之間的邊

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

正在做