Link-Cut Trees 小記


忘光啦 /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) 之類的)坐以待斃的話,在需要維護虛子樹的信息時就會 遺漏更新

  • rotatesplay 的寫法略與朴素 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);
      }
    }
    
  • 注意到上面寫法維護的信息需要滿足可減性。如果不滿足(比如子樹最值)那么可以使用些數據結構輔助維護。

  • 例題:Luogu P4219 [BJOI2014] 大融合

動態加邊維護重心

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) 找到這個根,然后這個根已經在 findRootsplay 上來了,於是直接找這個根的右兒子就是所求。
  • 例題: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)\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM