忘光啦 /dk /dk
本來 rotate 都會打錯的我現在終於會打啦於是 splay 打錯了
基礎要點
實鏈剖分
- 重鏈剖分是我們很熟悉的一個結構,它的優秀之處在於可以將樹上問題更加方便地轉化為序列問題(雖然也不全是)。然而注意到重剖只適用於靜態樹,如果將樹結構動態化那么重剖就不太行了。
- 於是引入一個更加靈活的鏈剖結構:實鏈剖分。它不依據大小,也不按照深度,虛實邊是人為規定的。
- 不過我們仍然需要一個數據結構來維護實鏈。為了適應實鏈剖分的靈活,我們選擇 Splay 作為輔助樹。
輔助樹
- 由於下面輔助樹可以表示出 唯一 的原樹,所以我們一般只維護這些輔助樹。
- 對於一條實鏈,顯然實鏈上的點深度連續,因此每棵 Splay 的 中序遍歷 的頂點序列深度是遞增且連續的。
- 虛邊在輔助樹中 認父不認子。具體的,在每棵 Splay 的根結點,其父指針(
fa)指向 當前所在鏈頂在原樹上的父親。
基礎操作
\(\text{access}\)
access(x)表示 打通 一條 \(x\) 到根的一條實鏈,實鏈以 \(x\) 為底,以根為頂。- 至關重要的操作之一,基本上是所有操作的基礎。
- 實現不復雜,一步步往上拉,具體分 4 步:
splay(轉到根);換兒子(換實邊);pushup(換了兒子要及時更新);跳fa。 -
inline void access(int x) { for (int p = 0; x; x = fa[p = x]) splay(x), ch[x][1] = p, pushup(x); }
\(\text{makeRoot}\)
- 若需要求出一條路徑的信息,那么需要兩點位於同一條實鏈上。然而這兩個點 可能並沒有祖孫關系,導致無法在同一個 Splay 中。
makeRoot(x)表示將 \(x\) 換為原樹的根。- 實現也很簡單:首先
access(x),這時發現這條實鏈和預期相比恰好是反的,於是 splay 上來后就打翻轉 Tag 就行了: -
inline void makeRoot(int x) { access(x), splay(x), setRev(x); }
\(\text{split}\)
split(x,y)表示隔離出一條以 \(x, y\) 為端點的實鏈。- 實現依靠前兩個操作:
makeRoot(x), access(y), splay(y);。操作之后 \(y\) 就包含了鏈上的信息。
\(\text{link}\)
link(x, y)表示加入一條 \(x, y\) 之間的邊。- 比較穩妥的寫法:先
makeRoot(x), makeRoot(y),接下來如果fa[x]是 0 那么 \(x, y\) 不連通,之間fa[x] = y即可。
\(\text{cut}\)
cut(x, y)表示切斷連接 \(x, y\) 的邊。- 比較穩妥的寫法:先
split(x,y),如果滿足ch[y][0] == x && !ch[x][1](說明 \(x, y\) 聯通且 \(x,y\) 路徑上不包含其他點)則表明 \(x,y\) 由一條邊直接相連,那么 將這條實邊直接斷掉 即可:fa[x] = ch[y][0] = 0, pushup(y); - 如果維護了 Splay 上的子樹大小也可以根據
split(x,y)出來的 Splay 樹大小判斷,如果大小為 2 說明直接相連。
其他次要操作
- 要找一個點所在原樹的根,需要先
access(x), splay(x),根據深度遞增的性質 易知原樹的根必然就是以 \(x\) 為根的 Splay 的最左側,找到后記得 splay 這個點保證復雜度。這個操作習慣稱為findRoot(x)。 - 判斷兩點連通性:可以
makeRoot(x)之后用上面的方法判斷findRoot(y)是否等於 \(x\) 即可。或者用link中的方法也行。不過對常數有要求時可以考慮並查集(如果可以)。 - 修改時需要 先 splay 上來,保證不會漏更新上面的點。
注意點
-
為了 保證復雜度,理論上每次在 Splay 中向下遍歷的行為都要
splay上來找到的點。一般而言,並不推薦在access之外的地方使用循環遍歷 Splay 樹結構的行為。當然有為了優化復雜度等目的使用高明方法的例外(動態加邊重心),具體問題具體分析。 -
小心操作的副作用!如上述的
findRoot需要為了保證復雜度但卻 改動了 Splay 的結構,這是一個很危險的信號。比如link如果直接使用findRoot判連通性的話,此時 \(y\) 不能保證還是 Splay 的根,如果不干其他補救措施(加一個splay(y)之類的)坐以待斃的話,在需要維護虛子樹的信息時就會 遺漏更新。 -
rotate、splay的寫法略與朴素 Splay 有別,這里直接貼:#define ident(x) (x == ch[fa[x]][1]) #define isRoot(x) (x != ch[fa[x]][0] && x != ch[fa[x]][1]) inline void rotate(int x) { int y = fa[x], z = fa[y], k = ident(x); if (!isRoot(y)) ch[z][y == ch[z][1]] = x; ch[y][k] = ch[x][k ^ 1], fa[ch[x][k ^ 1]] = y; ch[x][k ^ 1] = y, fa[fa[y] = x] = z; pushup(y), pushup(x); } void flush(int x) { if (!isRoot(x)) flush(fa[x]); pushdown(x); } inline void splay(int x) { flush(x); for (int y = fa[x]; y = fa[x], !isRoot(x); rotate(x)) if (!isRoot(y)) rotate(ident(y) == ident(x) ? y : x); } -
如果題目有強烈法方向性要求(比如 LCA 特判、有根樹等等),那么可以考慮 有根樹寫法的 LCT(上面是無根樹寫法)。具體的,沒有換根 & 翻轉標記;
link直接連虛邊;cut直接斷重邊。少去了很多花里胡哨的操作。但是注意,寫法簡潔、常數減小的同時也更需要斟酌其正確性。
常用技巧
邊權轉點權
- 如果不問點權而是邊權,怎么維護?
- 很簡單,把邊 拆 成一個點和兩條邊然后照做就行。注意防止 點對應的結點影響信息,於是可以初始化為無用值或
pushup特判等手段規避。 - 注意現在 LCT 中點數為 \(n+m\)。
- 例題:SPOJ QTREE。
動態加邊 MST
- 需要基於上面的技巧,維護邊權最大值及其 位置(假設求最小生成樹)。
- 現在來了一條邊 \((x,y)\) 權值為 \(w\)。先
split提取出 \(x,y\) 的路徑,然后求出路徑上所有邊的最大權值 \(w^\prime\)。如果 \(w<w^\prime\) 說明這條 新邊加上會更優,於是果斷將原來那個給cut了,link上新邊。最后得到的樹就是答案。 - 例題:Luogu P4234 最小差值生成樹。
動態加邊維護橋
- 給邊賦權:\(1\) 表示橋,\(0\) 表示不是橋。那么需要維護邊權和。
- 如果一條邊連接兩個原本不連通的點,那么
link上一條權值為 1 的邊。 - 反之則會出現一個環,而環上 不應存在任何一個橋。那么不用
link原本連接 \(x,y\) 的路徑上的所有邊都賦值為 0。 - 查詢兩點間橋的個數,直接就是路徑求和。
- 當然也有 FlashHu 那樣的 顯式縮點,常數小但容易寫錯。
- 例題:Luogu P2542 [AHOI2005] 航線規划。
維護子樹信息
-
遺憾的是 LCT 對於子樹就是短板了……但補救手段也不是沒有。
-
常見的處理方法額外維護 虛子樹 信息,然后在原來的寫法上略加改動:
inline void pushup(int x) { siz[x] = siz[ch[x][0]] + siz[ch[x][1]] + isiz[x] + 1;//此時的 siz 包含的虛子樹的大小 } inline void access(int x) { for (int p = 0; x; x = fa[p = x]) { splay(x); isiz[x] += siz[ch[x][1]]; isiz[x] -= siz[ch[x][1] = p]; // 及時切換貢獻 pushup(x); } } -
注意到上面寫法維護的信息需要滿足可減性。如果不滿足(比如子樹最值)那么可以使用些數據結構輔助維護。
動態加邊維護重心
Solution 1
- 考慮啟發式合並的思路:每次合並兩棵的樹,將小樹拆成單點一個個連到大樹上。由於一個點之后被合並 \(O(\log n)\) 次,所以復雜度為 \(O(n\log^2 n)\)。
- 那么怎么動態維護重心?考慮到一個性質:一棵樹加一個點,重心 最大移動一個距離。於是每次加點判斷一下暴力挪動即可。顯然挪動次數不超過加點次數,復雜度是對的。
Solution 2
-
充分利用重心的性質:兩棵樹合並,新重心必然在 原來兩個重心在新樹上的路徑上。
-
那么在
link之后先將這個連接重心的路徑split出來,然后做 平衡樹上二分。 -
具體的,需要維護好 子樹大小,然后維護兩個字段:
lsum表示當前搜索區間外左側的大小,同理定義rsum。 -
對於當前點 \(x\),如果說
lsum加上左子樹siz不超過一半,右邊也是,那么當前點必然可以作為一個重心。 -
然而我們似乎遺漏了當前點的
isiz,這部分會不會很大導致錯誤?回顧上面的性質,由於isiz必然不在這條重鏈 上,於是不必考慮。 -
找到之后記得及時
splay保證復雜度,總復雜度 \(O(n\log n)\)。// uset : 維護連通塊重心的並查集 inline void update(int u, int v) { split(u = find(u), v = find(v)); int x(v), lsum(0), rsum(0), tot(siz[x]), ret(N); while (x) { pushdown(x); int lcur(lsum + siz[ch[x][0]]), rcur(rsum + siz[ch[x][1]]); if (lcur * 2 <= tot && rcur * 2 <= tot) { if (tot & 1) { ret = x; break; } else { ret = std::min(ret, x); } } if (lcur < rcur) lsum = lcur + isiz[x] + 1, x = ch[x][1]; else rsum = rcur + isiz[x] + 1, x = ch[x][0]; } splay(ret); uset[u] = uset[v] = uset[ret] = ret; } -
例題:Luogu P4299 首都。
動態 dp
- 大致思路和重鏈剖分相似,適用維護子樹信息的技巧維護 \(f,g\) 兩個矩陣即可。
- 但要注意(廣義)矩陣乘法 沒有交換律,注意
pushup時的順序。 access時需要撤銷原來虛子樹的影響,直接根據矩陣的 實際意義 做即可。注意判空。- 一般來說復雜度為 \(O(n\log n)\) 乘上矩乘的復雜度,略優於樹剖。
- 例題:Luogu P5024 保衛王國
維護同色連通塊
- 有些題目要求維護點的顏色(一般種類不會很多)及其相關信息(比如同色連通塊大小)。
- 一個簡單的想法是,對於每條邊,只有在兩端同色時被連上。然而被 菊花圖 卡爆。
- 考慮對原樹定一個根,只連父邊,然后 開顏色種類個 LCT。對於每個 LCT 中的一個點,僅在其當前顏色的 LCT 中連父邊。
- 然后查詢時,要先砍掉當前連通塊的根。因為上面只連父邊導致會有一個 虛假的點(並不是這個顏色)出現在連通塊中,而這個點可能 聯立了其他連通塊,於是不能僅僅將大小減一。實際上也不是真正要執行
cut操作,注意到access(x)之后當前點已經在實鏈上了,於是我們先findRoot(x)找到這個根,然后這個根已經在findRoot中splay上來了,於是直接找這個根的右兒子就是所求。 - 例題:SPOJ QTREE6。
雜題選做
【HNOI2010】彈飛綿羊
有 \(n\) 個彈簧排成一排,每個彈簧有一個彈力系數 \(K_i\),表示從 \(i\) 可以彈到 \(i+K_i\) 的位置。\(Q\) 次操作,每次修改一個 \(K_i\),或詢問從 \(i\) 開始彈幾次被彈飛(彈到 \(>n\) 的位置)。
- 首先應該得認出這個 森林 的模型:對於每個點 \(i\),父節點為 \(i+K_i\),如果這個點之后被彈飛那么就是根。
- 發現這題有比較強的 方向性,考慮有根樹 LCT。
- 修改就是換父親,而查詢就是這個點在原樹中的 深度——也就是
access之后得到的 Splay 的siz。 - 當然也可以用一個虛點 \(n+1\) 作為 超級根 聯立森林,這樣就可以套有根 LCT 模板了。復雜度都是 \(O(n\log n)\)。
【CodeChef GERALD07】Chef and Graph Queries
給定一個 \(n\) 個點,\(m\) 條邊的無向圖,有 \(Q\) 次詢問,每次給定區間 \([l, r]\),求只保留編號在 \([l,r]\) 內的邊,形成的圖的連通塊個數。
- 一道神仙題(應該是 wtrl
- 首先考慮按編號將邊一條條加入,無非分兩種情況:這條邊聯通了兩個不同的連通塊,那么答案減一;否則這條邊與其他邊構成了環,那么對答案沒有貢獻。
- 那么如果知道了這個區間的邊一個個加入,那些邊構成了環就行。
- 着重思考成環邊:雖然構成一個環,不過如果這個環上有些 出現較早的邊因為區間性質不復存在,那么環上出現缺口,這條邊仍然產生貢獻。實際上一個環一旦存在一個缺口就會斷開兩個連通塊,於是不妨只考慮 出現最早的那條邊。每次轉化為求一條 鏈(當前邊尚未加入)上 編號最小的邊。
- 那么考慮預處理一個 \(\text{pre}_i\) 表示第 \(i\) 條邊在前面的一條邊 \(\text{pre}_i\) 不存在是它會有貢獻。用 LCT 維護 最大生成樹。
- 最后詢問可以看做是求區間 \([l,r]\) 內滿足 \(\text{pre}_i<l\) 的 \(i\) 的個數。二維數點,隨便搞。\(O(n\log n)\)。
