LCTの進階應用
關於LCT的進階應用
不光是寫演算法思路,因為已經有很多人寫過了,更重要的是程式碼寫法中的細節,不然LCT各種奇怪應用的細節夠你一題調一個小時
本篇文章將助你考試一遍過樣例!
當然,還有一些基礎應用比如維護路徑資訊或者維護聯通性,但是程式碼都相對比較模板,所以也沒什麼好寫
一. LCT維護邊雙連通分量
有些題目中會動態加邊並有形如“x到y之間有多少必經邊” “某個邊雙內有多少點”的詢問,這時候需要用LCT來維護邊雙聯通分量
思路講解
注意到,如果把所有邊雙縮成點,那麼一定會構成一個森林
此時如果新增了一條邊 \(x,y\),一種情況是 \(x,y\) 不在同一棵樹裡,此時直接link(x,y)
一種情況是 \(x,y\) 在同一棵樹中,但不在同一個邊雙中,那麼就會形成一個新的邊雙
找出 \(x,y\) 所在的邊雙所縮成的點 \(fx,fy\),然後把 \(fx\) 到 \(fy\) 這條路徑上的所有點縮成一個新點,並讓這個新點來記錄新的邊雙中的點的權值之和
實現時可以用並查集來記錄每個點在哪個邊雙中
注意,維護邊雙時不支援刪邊操作,因為無法快速確定刪除某個邊雙內部的一條邊後會分裂成幾個新的邊雙
程式碼剖析
相信大家都已經記住原版LCT是怎麼寫的了
這裡來看一下維護邊雙的LCT和原版有哪些區別
1. 並查集維護每個點在哪個邊雙中
int Fa[N]; int find(int x) { return Fa[x] == x ? x : Fa[x] = find(Fa[x]); } //記得初始化並查集
2. access操作寫法發生變化
inline void access(int x) {
for (int i = 0; x; i = x, x = find(fa[x])) { //x每次跳到find(fa[x])而不是fa[x]
splay(x); ch[x][1] = i;
if (i) fa[i] = x; //一定要把新的重兒子i的父親設為x
pushup(x);
}
}
3. 新增操作merge:將x到y的路徑縮為一個點
queue<int> q; inline void merge(int x, int y) { split(x, y); //將x到y的路徑提取出來 q.push(y); while (!q.empty()) { //用bfs在二叉樹上遍歷x到y路徑上的所有點 int u = q.front(); q.pop(); Fa[find(u)] = y; //維護並查集 if (ch[u][0]) q.push(ch[u][0]); if (ch[u][1]) q.push(ch[u][1]); ch[u][0] = ch[u][1] = 0; } val[y] = sum[y]; //y成為這個邊雙的代表元 //如果還有其它資訊也是要全部讓y來存 }
4. 連邊 \(x,y\) 時的分類討論
虛擬碼如下 維護兩點聯通性可以再開一個並查集,也可以用findroot
void LINK(int x, int y) {
int fx = find(x), fy = find(y);
if (fx == fy) return; //x,y已在同一邊雙中
if (x,y不在同一棵樹中) {
link(x,y); //如果用另一個並查集維護聯通性記得更新並查集
} else {
merge(fx,fy); //是fx,fy 不是x,y!!!
}
}
5. 進行任何修改/詢問操作時都一定是對 \(fx=find(x)\) 進行,而不是 \(x\) 本身!
例題
二. LCT維護子樹資訊
LCT一般用來維護路徑資訊而不是子樹資訊,因為LCT維護子樹資訊非常不方便。。。
但是有些毒瘤題可能在詢問子樹資訊的同時還有加邊刪邊操作,或者可能有換根操作,就只能被迫使用LCT了
思路講解
假設現在需要用LCT維護原樹中一個子樹的 \(siz\)
為了方便查詢,一般會把原樹中一條重鏈的頂端 \(x\) 的子樹資訊儲存在 (\(x\) 在LCT中所在的二叉樹的根) 的位置
如上圖,原樹中的 \(siz[1]\) 實際上存在LCT中的 \(siz[3]\) 裡
但是一個點子樹的 \(siz\) 不僅包含自己所在的那條重鏈的 \(siz\) 啊 如何維護輕子樹的大小?
我們記 \(lsiz[x]\) 表示LCT中 \(x\) 的所有輕子樹的 \(siz\) 之和,這樣 \(siz[x]\) 就等於 \(siz[lson]+siz[rson]+lsiz[x]+1\)
如果我們希望查詢 \(x\) 點的子樹資訊,只需要access(x)
,然後此時 \(x\) 就會位於一條重鏈的底端,那麼 \(siz[lson]\) 和 \(siz[rson]\) 都為 \(0\),\(siz[x]=lsiz[x]+1\),而此時LCT中 \(x\) 的輕子樹一定一一對應著原樹中 \(x\) 的子樹,所以此時的 \(siz[x]\) 就是 \(x\) 在原樹中的 \(siz\)
如果需要查詢以 \(x\) 為根時整棵子樹的資訊,只需makeroot(x)
,然後直接查詢
接下來的問題就在於如何維護這個 \(lsiz\) 了 來看一下程式碼
程式碼剖析
1. pushup操作寫法發生變化
inline void pushup(int x) {
sum[x] = sum[ch[x][0]] + sum[ch[x][1]] + lsum[x] + val[x]; //lsum表示輕子樹的權值和
}
2. access操作寫法發生變化
inline void access(int x) {
for (int i = 0; x; i = x, x = fa[x]) {
splay(x);
lsum[x] += (siz[ch[x][1]] - siz[i]); //ch[x][1]變為輕兒子,而i不再是輕兒子
ch[x][1] = i; pushup(x);
}
}
3. link操作寫法發生變化
inline void link(int x, int y) {
makeroot(x); makeroot(y); //x,y都要makeroot!
fa[x] = y;
lsum[y] += sum[x]; //x成為y的輕兒子
pushup(y);
}
4. 單點修改 \(x\) 時先 makeroot(x)
!
例題
3. LCT維護形態樹+權值樹
這類題目一般是要求維護樹上路徑資訊,但是經常會對路徑上的點權做一些奇怪的操作
如[BZOJ3159]決戰:路徑翻轉操作
[GDSOI2017]中學生資料結構題:路徑迴圈移位操作
思路講解
眾所周知,LCT上的Splay以原樹中的節點深度為關鍵字,隨便你怎麼rotate
,只要節點的相對次序不變,那麼就只有輔助樹的形態會發生改變,而對應的原樹形態不變
在makeroot(x)
操作中,我們翻轉了以 \(x\) 為根的Splay,節點的相對次序發生了改變,所以其實在原樹中就體現為原樹的根變成了 \(x\)
但是在這種題裡,如果你還是為了維護權值胡亂操作輔助樹Splay,那說不定什麼時候你就不小心把哪個點變成原樹的根了。。。
所以我們要再建出一棵輔助樹來維護權值,使得在這棵輔助樹上進行操作一定不會影響原樹形態,我們把這棵輔助樹叫做權值樹,而LCT那棵叫形態樹
這裡我選擇了非旋Treap來維護權值樹
現在需要解決的問題就是 我們希望形態樹和權值樹是時刻對應的
比如說,現在LCT被分為這樣幾棵Splay:\([1,2],[3,5,6],[4,7]\),那麼此時權值樹一定也是被這樣劃分為3棵的
還有一個東西也要對應,就是形態樹的中序遍歷序列要時刻與權值樹的中序遍歷序列相同,這樣才能夠保證形態樹中某棵平衡樹排名第 \(k\) 的點和權值樹中對應平衡樹的第 \(k\) 個點是同一個點,方便進行修改
比如說此時形態樹中3棵平衡樹的中序遍歷分別為 \([1,2],[6,3,5],[7,4]\),那麼權值樹也要一樣,絕不可能是 \([1,2],[5,3,6],[4,7]\)
為了實現這個功能,我們需要在LCT更改輕邊重邊時同樣維護權值樹的連邊
同時為了方便查詢,還需要動態維護每棵形態樹中的平衡樹對應著權值樹中的哪一棵平衡樹
用 \(rt[x]\)來維護這個資訊,一定要保證形態樹中每棵平衡樹的樹根的 \(rt[x]\) 是權值樹對應的那棵平衡樹的樹根,這樣才能正確修改和查詢
程式碼剖析
1. 權值樹該怎麼寫怎麼寫,寫一個正常的平衡樹就行
2. splay操作寫法發生變化
inline void splay(int x) {
q[top=1] = x;
int i; for (i = x; !isroot(i); i = fa[i]) q[++top] = fa[i];
swap(rt[i], rt[x]); //x將會變成平衡樹的根,把原來根的rt值給x
while (top) pushdown(q[top--]);
while (!isroot(x)) {
int y = fa[x], z = fa[y];
if (!isroot(y)) ((ch[z][1] == y) ^ (ch[y][1] == x)) ? rotate(x) : rotate(y);
rotate(x);
}
}
3. 劃重點!access操作寫法
inline void access(int x) {
for (int i = 0; x; i = x, x = fa[x]) {
splay(x);
if (ch[x][1]) {
VAL::split(rt[x], siz[x] - siz[ch[x][1]], rt[x], rt[ch[x][1]]);
//x的重兒子不再是ch[x][1],把它的子樹從x對應的平衡樹中刪去
}
if (i) {
rt[x] = VAL::merge(rt[x], rt[i]);
//i成為x的重兒子,把它的子樹加入x對應的平衡樹
//注意merge順序
}
ch[x][1] = i; pushup(x);
}
}
3. makeroot操作寫法發生變化
inline void makeroot(int x) {
access(x); splay(x); rev(x);
VAL::Rev(rt[x]); //為了保證中序遍歷相同,權值樹也要翻轉
}
4. 對於任何修改/查詢,先split(x,y),然後在權值樹的對應平衡樹上修改/查詢
inline void Add(int x, int y, ll v) { split(x, y); VAL::Add(rt[y], v); }
inline void Rev(int x, int y) { split(x, y); VAL::Rev(rt[y]); }
inline ll Qsum(int x, int y) { split(x, y); return VAL::sum[rt[y]]; }
inline ll Qmax(int x, int y) { split(x, y); return VAL::mx[rt[y]]; }
inline ll Qmin(int x, int y) { split(x, y); return VAL::mn[rt[y]]; }
例題
4. LCT維護邊權資訊
LCT怎麼維護邊權?LCT維護不了邊權。
但是可以把邊拆成點,然後就變成維護點權了(
多用於維護生成樹
思路講解
新科技:KrusLCT演算法 \(O(m\log m)\) 求最小生成樹!
對於一條邊 \((u,v)\),新建一個點 \(w\),把 \(w\) 的點權設為邊權,\(u,v\) 的點權視題目設成正無窮,負無窮,\(0\) 之類的,然後連邊 \((u,w)\),\((w,v)\)
這樣有什麼好處呢?如果我想要查詢 \(x\) 到 \(y\) 路徑上的最大邊權,只需要在LCT上查詢 \(x\) 到 \(y\) 的最大點權就好了
所以可以口胡出一個最小生成樹演算法:
按順序考慮每一條邊 \(u,v\):若 \(u,v\) 不連通,則在LCT中連上 \(u,v\)
否則找出LCT上 \(u,v\) 路徑上的最大點權,若這個"點"權大於當前邊 \(u,v\) 的邊權,就把那條邊斷開,連上這條邊
當然這就是LCT維護邊權的一個應用,其實理解起來非常簡單
程式碼剖析
實在是沒有什麼好寫的了 因為和普通的LCT沒有什麼區別,唯一的區別就是連邊/刪邊要刪兩條?
1. 我到底應該刪哪兩條邊?
//使用 map<pair<int, int> , int> !!!