LCT入門教程


原文鏈接 https://www.cnblogs.com/zhouzhendong/p/LCT.html

由於我感覺之前的ppt版過於愚蠢而且之前使用的編輯器不是 Markdown,所以我把它變成了網頁版。

LCT 入門總結

問題模型

給定一棵森林,每一個點有一個權值。請你支持以下操作:

  • 單點修改權值。
  • 詢問一條鏈點權和。
  • 刪除一條邊。
  • 修改一條邊。

設點數和操作數都為 \(n\),則要求做到 \(O(n\log n)\) 的時間復雜度。

什么是 LCT

LCT 全稱 Link-Cut Tree ,用於處理一類動態樹問題。

LCT 將所有的邊分成實邊和虛邊兩類,形成若干條實鏈,並對此建立一個輔助樹。

對於每一條實鏈,以節點深度為關鍵字將節點用splay存起來。

一個輔助樹例子

其中紅色的是實邊,黑色的是虛邊。

那么在原樹中,2 的父親是 8 ,7 的父親是 1 。其中用灰色箭頭連接起來的 1-7-5-8-2-6 是一條樹鏈。

下圖就是上圖對應的原樹。

如何維護 LCT

前置技巧 splay

Access

Access(x) 的作用是打通一條從節點x 到達連通塊根的路徑。

例如在下圖中 Access(8)

令初始時 tmp = 0(注意此時 \(x = 8\)

第一步,把節點x splay 到它所在的實鏈對應的平衡樹頂端。

然后把 tmp 接到 x 以下。等價於將實鏈中,深度大於 x 的部分切斷。然后令 tmp = x, x = fa[x]; 此時 \(x = 1\),於是接下來對於節點1 做 splay,也切斷深度大於 1 的一部分,再將 tmp 接上去。不斷向根循環進行操作,最終得到下圖(結合代碼理解更佳):

性質

如上圖所示, Access(8) 之后, 節點8 一定是這條實鏈的最深節點。如果將他 splay 到根,那么它一定沒有右兒子。

代碼

void access(int x){
    int t=0;
    while (x){
        splay(x),son[x][1]=t;
        t=x,x=fa[x];
    }
}

換根(rever)

rever(x) 的作用是將 x 變成連通塊的根。

代碼

void rever(int x){
    access(x),splay(x),rev[x]^=1;
}

簡單解釋

access(x):打通到根路徑。splay(x):將 x 提到根(注意這時候 x 是沒有右子樹的)。由於這里是換根,如果這樣就結束了,那么 x 的深度沒有變。既然換根,那么也要換深度,所以打上翻轉標記來實現深度翻轉。

link

link(x,y) 即在 x 與 y 之間連邊。

代碼

void link(int x,int y){
    rever(x),fa[x]=y;
}

簡單解釋

將 x 變成根之后, x 就沒有 father 了,這樣就取消了 x 的 father 的干擾;然后再建立 x->y 的虛邊。

cut

代碼

void cut(int x,int y){
    rever(x),access(y),splay(y),fa[x]=son[y][0]=0;
}

簡單解釋

rever(x): 將 x 變成該連通塊的根。

access(y): 打通從 y 到該連通塊根 x 的一條路徑。

splay(y): 將 y 已到輔助樹根位置。

這時,由於 x 和 y 在同一連通塊切有直接連邊,又因為 x 的深度比 y 小,所以在輔助樹中 x 必然是 y 的左子節點,直接斷開就好了。

時間復雜度證明

https://www.cnblogs.com/zhouzhendong/p/JunTanFenXi.html

一個維護樹鏈權值和的例子

int n,m;
int fa[N],son[N][2],rev[N],val[N],sum[N];
bool isroot(int x){
    return son[fa[x]][0]!=x&&son[fa[x]][1]!=x;
}
void pushup(int x){
    sum[x]=sum[son[x][0]]+sum[son[x][1]]+val[x];
}
void pushdown(int x){
	if (rev[x])
	    rev[son[x][0]]^=1,rev[son[x][1]]^=1,swap(son[x][0],son[x][1]),rev[x]=0;
}
void pushadd(int x){
    if (!isroot(x))
        pushadd(fa[x]);
    pushdown(x);
}
int wson(int x){
    return son[fa[x]][1]==x;
}
void rotate(int x){
    if (isroot(x))
        return;
    int y=fa[x],z=fa[y],L=wson(x),R=L^1;
    if (!isroot(y))
        son[z][wson(y)]=x;
    fa[x]=z,fa[y]=x,fa[son[x][R]]=y;
    son[y][L]=son[x][R],son[x][R]=y;
    pushup(y),pushup(x);
}
void splay(int x){
    pushadd(x);
    for (int y=fa[x];!isroot(x);rotate(x),y=fa[x])
        if (!isroot(y))
            rotate(wson(x)==wson(y)?y:x);
}
void access(int x){
    int t=0;
    while (x){
        splay(x);
        son[x][1]=t;
        pushup(x);
        t=x;
        x=fa[x];
    }
}
void rever(int x){
    access(x);
    splay(x);
    rev[x]^=1;
}
void link(int x,int y){
    rever(x);
    fa[x]=y;
}
void cut(int x,int y){
    rever(x);
    access(y);
    splay(y);
    fa[x]=son[y][0]=0;
}

例題

[HNOI2010] 彈飛綿羊

沿着一條直線有n個裝置,每個裝置設定初始彈力系數ki,當綿羊達到第i個裝置時,它會往后彈ki步,達到第i+ki個裝置,若不存在第i+ki個裝置,則綿羊被彈飛。當它從第i個裝置起步時,被彈幾次后會被彈飛?此外,還會中途修改某個彈力裝置的彈力系數,任何時候彈力系數均為正整數。

\(n\leq 10^5\)

提示

我們把彈簧裝置看做 x 的效果看做一條從節點 i 到節點 i+ki 的邊,那么必然會得到一棵樹。

我們設一個總的根節點 root 表示彈飛之后到達的節點,那么顯然節點 i 的答案就是 i 到 root 的距離。

代碼

http://www.cnblogs.com/zhouzhendong/p/8028221.html

[SDOI2008] Cave

有 n 個點,一開始沒有連邊。有 m 次操作:

操作有 3 種類型:一種是連接某兩個點,一種是斷開某一條邊。還有一種是詢問兩個點是否連通。
操作過程中保證整個圖是森林。

\(n\leq 10^4,m\leq 2\times 10^5\)

提示

主要問題是判斷兩個點是否在同一個連通塊里。注意到如果他們在同一個連通塊,那么他們的根是相同的。

判斷 x y 是否在同一個連通塊的做法很多,隨便說幾種:

  • rever(x); access(y); splay(y); 找到 y 對應的splay里優先級最高的點看看是不是 x 就好了。(就是不斷往左節點跳)
  • access(x); splay(x); 找到 x 對應的splay里優先級最高的點 k1; access(y); splay(y) 找到 y 對應的splay里優先級最高的點 k2 ,判斷 k1 是否等於 k2 即可。

“找優先級最高的點”本質是找原樹的根。

代碼

http://www.cnblogs.com/zhouzhendong/p/8029252.html

BZOJ2843 極地旅行社

有n座島,每座島上的企鵝數量雖然會有所改變,但是始終在[0, 1000]之間。你的程序需要處理以下三種命令:

  1. "bridge A B"——在A與B之間建立一座大橋(A與B是不同的島嶼)。由於經費限制,這項命令被接受,當且僅當A與B不聯通。若這項命令被接受,你的程序需要輸出"yes",之后會建造這座大橋。否則,你的程序需要輸"no"。
  2. "penguins A X"——根據可靠消息,島嶼A此時的帝企鵝數量變為X。這項命令只是用來提供信息的,你的程序不需要回應。
  3. "excursion A B"——一個旅行團希望從A出發到B。若A與B連通,你的程序需要輸出這個旅行團一路上所能看到的帝企鵝數量(包括起點A與終點B),若不聯通,你的程序需要輸出"impossible"。

提示

直接多維護一個樹鏈和就好了。

只需要在改變輔助樹實鏈形態的時候pushup就好了。

代碼

http://www.cnblogs.com/zhouzhendong/p/8033088.html

BZOJ2631

一棵n個節點的樹,節點有權值,m次操作。

要支持: 刪邊、連邊、區間求和、區間加、區間乘。

保證操作過程中不出現環。

\(n,m\leq 100000\)

提示

把你學過的線段樹的pushup放到lct上就好了。

代碼

http://www.cnblogs.com/zhouzhendong/p/8038368.html

BZOJ2594 [Wc2006] 水管局長數據加強版

n個點的圖,m條帶權邊,有q次操作

操作有兩個類型:

  1. 在節點x到y的之間所有路徑中找一條路徑,使得這條路徑上的最大邊權盡量小,輸出這個最小值。
  2. 刪除某一條邊

操作過程中保證圖連通。

\(n,q\leq 10^5,m\leq 10^6\)

提示

這題需要用到“時光倒流”。

首先,我們先假裝邊都刪好了,然后倒過來一邊加邊,一邊回答。

其次,我們要找的這條邊,肯定在當前最小生成樹中 x 到 y 路徑上的。

於是我們需要維護邊權 max 。但是到現在位置,我們所知 LCT 只能維護點權 max。

只需要把邊看做一個點,這個點向這條邊連接的兩個點各連一條邊即可。

代碼

http://www.cnblogs.com/zhouzhendong/p/8041313.html

BZOJ3514 Codechef MARCH14 GERALD07加強版

N個點M條邊的無向圖,詢問保留圖中編號在[l,r]的邊的時候圖中的聯通塊個數。

\(N,M,Q\leq 200000\)

提示

  1. 首先,將邊按照編號順序依次加入圖中。每當出現環時,將環中最早加入的邊彈出。記第 i 條邊被彈出的時間為 \(T_i\) ,表示在第 \(T_i\) 條邊加入的時候,第 i 條邊被彈出了。
  2. 詢問一段區間的邊出現的情況下的 連通塊個數,可以轉化成 點數 - 任意一個生成森林的邊數。那么如何求邊數?對於一對 \([L,R]\) ,滿足 \(L\leq i\leq R, R < T_i\) 的點數。所以只要主席樹維護一下就好了。

代碼

由於寫代碼在很久以前,所以代碼和這里的做法簡述可能有些偏差,但無傷大雅。

http://www.cnblogs.com/zhouzhendong/p/8042515.html

UOJ#207. 共價大爺游長沙

給定一個 n 個節點的樹,有 m 次操作,操作有以下 4 種類型:

  1. 給定四個正整數 x,y,u,v,表示先刪除連接點x和點y的無向邊,保證存在這樣的無向邊,然后加入一條連接點u和點v的無向邊,保證操作后的圖仍然是一棵樹。
  2. 給定兩個正整數 x,y,表示在 S 中加入點對 (x,y)。
  3. 給定一個正整數 x,表示刪除第 x 個加入 S 中的點對,即在第 x 個操作2中加入 S 中的點對,保證這個點對存在且仍然在 S 中。
  4. 給定兩個正整數 x,y,詢問連接點 x 和點 y 的邊是否屬於 S 集合中的所有路徑的交集,保證存在這樣的無向邊且此時 S 不為空。

\(n\leq 10^5,m\leq 3\times 10^5\)

提示

首先,我們發現詢問一條邊(x,y)是否是所有路徑的交集,可以轉化成:

將 x 變成根,詢問在S集合中的每一條路徑是否都有且僅有一個端點出現在 y 子樹中。

於是,問題轉化成了維護子樹信息。對於每一條路徑的兩端點,我們 random 一個值,並 xor 給這兩個端點,於是原問題就變成了 LCT 維護子樹節點權值 xor 。

LCT 維護子樹信息和維護樹鏈信息有些差別。我們在 LCT 的時候,再對於每一個節點維護其虛兒子的信息。由於 LCT 涉及修改虛兒子的操作十分少,所以只需要在修改邊的虛實關系的時候順便維護一下就好了。

代碼

https://www.cnblogs.com/zhouzhendong/p/UOJ207.html

更多習題

BZOJ3091 城市旅行
BZOJ2759 一個動態樹好題
BZOJ4025 二分圖(這題也有其他做法)

鳴謝

感謝 cly 大爺強行要學動態dp,蒟蒻zzd也去學了學,順便學了全局平衡二叉樹才發現我之前的LCT博客寫錯了。

感謝 emoarix 在很久以前提供的例題一道 BZOJ4025。

感謝 Yang Zhe《SPOJ375 QTREE 解法的一些研究》使我從中獲益。


免責聲明!

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



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