動態樹 之 Link-Cut Tree (LCT)
自此 Frocean 不想搞資料結構了(當然熟練一下LCT還是要的) 暫時是夠用的了
2017年9月 初三 退出腐敗界潛心搞OI 當然已經欠得很多了
2018年4月 當時剛學完線段樹 然後總是聽見機房dalao們喊樹剖 於是去找樹剖題 順便請教了某dalao 然後該dalao看了跟我說這類題目LCT都能做 我:"那我要學LCT惹~" 然而又要先搞樹剖
於是概念之類的東西瞎折騰了兩週多 然後程式碼交到改對又花了大概一週(真是弱啊 現在我LCT兩天就弄完= =)
然後中途忘掉了這東西 做了些水題鞏固基礎 外加一些其他基礎演算法
2018年8月 來雙鴨山訓練的時候聽說A組一天一個LCT 突然想起來這事 然後瞎搞與LCT有關的splay結果被教練抓住叫去弄USACO題庫
2018年9月 splay搞定了 然而區間翻轉還是不會 但是LCT要用到
2018年10月 splay區間翻搞定了 然後31號開始看LCT
2018.11.1 LCT搞定了 當然現在還不熟
現在想想為了個LCT我學的東西還挺多的 然而同樣是平衡樹Treap我卻沒學
好了心路歷程記完了 現在開始說說LCT
前置知識 Splay(力推我的) + 樹鏈剖分(最好理解) 我當諸君都很熟練了啊QAQ
原理概念之類的戳這裡 他講的挺不錯的 然後我主要記記操作&程式上
話說LCT裡相關操作的子程式好多 講起來比較亂 首先規定一個模板題 然後相關基礎操作圍繞此題展開
先把核心操作丟上面 然後分詢問要求放相關操作 建議順序:Link—>delete—>change(date)—>query(get) 然後多翻幾遍核心操作啦
注:個人習慣原因有些子程式名字會相替換 個人的丟括號裡面
1.isroot(pdroot)
這個就是判斷當前點是不是他所在的 splay 的根了啦 其實我也不想放前面但後面常常用到
如果一個點是根 那他父親就和他沒什麼關係了 在樹上一大把 splay 的 LCT 上 連線他們的只是各個根的父親丟到上面的 splay 裡
所以一個根的父親的兒子不會指向他!我們可以利用這個性質判斷一個點是不是他所在 splay 的 根
程式碼只有一行 見下(說四行的我不打你)
inline short pdroot(int x)
{
return (e[e[x].fa].son[0] != x) && (e[e[x].fa].son[1] != x);
}
然後我先把 splay 相關丟出來好了 這樣看著 LCT 親切點
2.pushup(update)
這個地方除了rotate其他時候也要用到哦 因為不止這地方換了父子從屬關係
所以打成了個子程式= = 本題裡就是維護路徑的異或和
下放程式碼
inline void update(int x)
{
e[x].ans = e[e[x].son[0]].ans ^ e[e[x].son[1]].ans ^ e[x].v;
}
3.pushdown
就是下放旋轉的 lazy 標記
和splay區間翻的完全一樣啦
下放程式碼
inline void pushdown(int x)
{
if (!e[x].laz) return;
int bot = e[x].son[0];
e[x].son[0] = e[x].son[1];
e[x].son[1] = bot;
e[e[x].son[0]].laz ^= 1;
e[e[x].son[1]].laz ^= 1;
e[x].laz = 0;
}
4.relax(change)
這個就是在換父子關係前同一將涉及到的點的lazy標記下放
因為某些原因我們只能從底下那個點找起 但是標記更新要從最上面啊
所以 遞迴 然後從最上面更新 這裡利用pushdown
其實pushdown和relax只有splay裡面涉及到的惹= =其他地方不用(其他地方用splay2333)
下放程式碼
inline void change(int x)
{
if (!pdroot(x)) change(e[x].fa);
pushdown(x);
}
5.splay
許多人的 splay (包括我) 的迴圈判斷都是 父節點不為0的時候往下跑
但這裡很多 splay 而且判斷父節點是不是出去了很麻煩
於是判斷當前點是不是根就好啦 往上翻 1.isroot 其他照常 splay
下放程式碼
inline void splay(int x)
{
change(x);
while (!pdroot(x))
{
int y = e[x].fa,z = e[y].fa;
if (!pdroot(y)) {
if ((e[z].son[0] == y) ^ (e[y].son[0] == x)) rotate(x);
else rotate(y);
} rotate(x);
}
}
6.rotate
對了就是熟悉的 rotate 但是有不同的地方 (x是當前點 y是父親 z是祖父)
這裡關於z的話 如上 1.isroot 中所說 如果 z 和 x 不是同一 splay 中的話 z 的孩子就不用更新成 x
所以這裡丟個判定就好了啦 然後....這類題目不用維護 size 喜大普奔 但是求答案還是要維護下
下放程式碼 (關於求答案 到時候 access 從根連起來直接找根的答案陣列就可以輸出了)
inline void rotate(int x)
{
int y = e[x].fa,z = e[y].fa,mode = e[y].son[0] == x;
if (!pdroot(y)) e[z].son[e[z].son[1] == y] = x;
e[x].fa = z;
e[y].son[mode ^ 1] = e[x].son[mode];
e[e[x].son[mode]].fa = y;
e[x].son[mode] = y;
e[y].fa = x;
update(y); //update先後關係注意 和splay一樣先子後父
update(x);
}
好了splay相關講完了 下面就是LCT的核心部分了(上面也包括了的哈)
7.access
這個大有用處了 作用是把某點和原樹的根所在路徑變成重邊
然後因為性質之類的 需要把原重邊 (如果不一樣) 換成輕邊
這個用處= =之後會提到的 就是方便處理問題(因為要處理的兩個數會是打通的重邊的兩個最外面的點)
然後怎麼更新呢 首先斷掉要通到的點的兒子
然後我們要splay一下 因為這樣可使連著當前點的重邊在點的右兒子處
然後更新當前點兒子 這裡和首先斷點可以連起來 設個變數一開始為0 然後第一次更新的點重兒子通向0 就是無重兒子了
很巧妙 然後後面把當前點記錄 之後推到他父親的時候把他父親的重兒子記錄到他上面 再更新他
splay之後的更新也會使當前點的兒子變化 所以要update當前點的異或和的答案
下放程式碼
inline void access(int x)
{
for (int y = 0 ; x ; y = x,x = e[x].fa)
{
splay(x);
e[x].son[1] = y;
update(x);
}
}
8.makeroot
就是把某個點變成他所在的splay的根啦
首先打通他到原樹根的路徑 使得他為全樹深度最大 然後把他弄到根 這時候他是沒有右兒子的
然後為了處理該點和另外一個點的相對關係 (對 makeroot就是讓這兩點產生更親♂密的關係的)
我們用access和splay把當前點通到根 然後還要預翻轉一下 設當前點為 x 另一點為 y
這樣使得處理 y 點時 因為處理 y 點時必有access使得之後的splay變成兩點中的唯一簡單路徑
然後splay把 x + 1 到 y 翻轉 y 就是 x + 1 (在那個地方) 於是可以通過父子關係處理操作
絕妙啊 不說了丟程式碼
inline void makeroot(int x)
{
access(x);
splay(x);
e[x].laz ^= 1;
}
好了好了接下來依次處理本題的詢問 首先是link的兩個
9.findroot(f)
就是找當前點所在的splay的root啦
首先打通 然後當前點旋到根 然後.......
顯然是深度往上跳嘛 那就找左兒子啦 深度減小嘛
直到找到某點兒子不是他 說明他兒子和他是屬於不同的樹了的 返回他即可
下放程式碼
inline int f(int p)
{
access(p),splay(p);
while (e[p].son[0]) p = e[p].son[0];
return p;
}
10.link
就是連線兩點嘛 首先先判兩點是否聯通 如果聯通還連邊那這種題是不能用LCT做的惹qwq
然後讓其中一個點旋到根 那他是沒父親的了
把他父親變成另外一個點 那就連起來了啦
另外一個點的兒子不用更新 不然原本的就亂了 這個等之後處理其他操作的時候整理就好
下放程式碼
inline void link(int x,int y)
{
if (f(x) != f(y)) makeroot(x),e[x].fa = y;
}
11.delete(dele)
刪邊 同link一樣要先makeroot其中一個點
但刪邊對兩點的相對關係要求更多 我們要把兩點弄到一塊
對了makeroot裡面翻轉使得兩點丟到一塊了
設兩點為 x 點和 y 點
於是 此時 x 變成根了 然後 y 的深度就是 x + 1
但是這樣很可能 y 在 x 的右兒子的左子樹裡面
所以 y 旋上去 然後比 y 小的 只有 x 於是 x 必定是其左兒子
先判定一下是不是 x 嘛 如果不是說明 (x,y) 這條邊不存在
然後父子關係都清零就好了
下放程式碼
inline void dele(int x,int y)
{
makeroot(x);
access(y);
splay(y);
if (e[y].son[0] == x) e[y].son[0] = 0,e[x].fa = 0;
}
12.modify(date)
原本想用update的但是重名了= =
就是更新點權 超簡單的
當前點權直接換掉 旋到根
splay經典操作不多講了
下放程式碼
inline void date(int x,int y)
{
e[x].v = y;
splay(x);
}
13.query(get)
好了最後的查詢
一個點丟到根上面 makeroot大法好
access打通到另一個點 然後兩點間簡單路徑的異或和就是答案~~
但是!這兩操作是把鏈提取出來了 但我們要得到整棵splay的答案 就需要splay另一個點 然後再返回根節點的值
所以還要把另一個點旋上來 輸出這個點的答案
下放程式碼
inline int get(int x,int y)
{
makeroot(x);
access(y);
splay(y);
return e[y].ans;
}
好了好了總算完了=-= 下面塞個總的程式碼 模板這裡再丟一個
#include <cstdio>
#define I inline
#define like int
#define love void
using namespace std;
const int MAXN = 300010;
struct lct {
int fa,son[2],ans,laz,v;
} e[MAXN];
I like r()
{
char q = getchar(); int x = 0,y = 0;
while (q < '0' && q != '-' || q > '9') q = getchar();
if (q == '-') ++ y,q = getchar();
while ('0' <= q && q <= '9')
x = (x << 3) + (x << 1) + q - (3 << 4),q = getchar();
return y ? -x : x;
}
I like pdroot(int x)
{
return e[e[x].fa].son[0] != x && e[e[x].fa].son[1] != x;
}
I love pushdown(int x)
{
if (!e[x].laz) return;
int bot = e[x].son[0];
e[x].son[0] = e[x].son[1];
e[x].son[1] = bot;
e[e[x].son[0]].laz ^= 1;
e[e[x].son[1]].laz ^= 1;
e[x].laz = 0;
}
I love change(int x)
{
if (!pdroot(x)) change(e[x].fa);
pushdown(x);
}
I love update(int x)
{
e[x].ans = e[e[x].son[0]].ans ^ e[e[x].son[1]].ans ^ e[x].v;
}
I love rotate(int x)
{
int y = e[x].fa,z = e[y].fa,mode = e[y].son[0] == x;
if (!pdroot(y)) e[z].son[e[z].son[1] == y] = x;
e[x].fa = z;
e[y].son[mode ^ 1] = e[x].son[mode];
e[e[x].son[mode]].fa = y;
e[x].son[mode] = y;
e[y].fa = x;
update(y);
update(x);
}
I love splay(int x)
{
change(x);
while (!pdroot(x))
{
int y = e[x].fa,z = e[y].fa;
if (!pdroot(y)) {
if ((e[z].son[0] == y) ^ (e[y].son[0] == x)) rotate(x);
else rotate(y);
} rotate(x);
}
}
I love access(int x)
{
for (int y = 0 ; x ; y = x,x = e[x].fa)
{
splay(x);
e[x].son[1] = y;
update(x);
}
}
I like f(int p)
{
access(p),splay(p);
while (e[p].son[0]) p = e[p].son[0];
return p;
}
I love makeroot(int x)
{
access(x);
splay(x);
e[x].laz ^= 1;
}
I like get(int x,int y)
{
makeroot(x);
access(y);
splay(y);
return e[y].ans;
}
I love link(int x,int y)
{
if (f(x) != f(y)) makeroot(x),e[x].fa = y;
}
I love dele(int x,int y)
{
makeroot(x);
access(y);
splay(y);
if (e[y].son[0] == x) e[y].son[0] = 0,e[x].fa = 0;
}
I love date(int x,int y)
{
e[x].v = y;
splay(x);
}
int main()
{
int n = r(),m = r();
for (int a = 1 ; a <= n ; ++ a) e[a].ans = e[a].v = r();
while (m--)
{
int mode = r(),x = r(),y = r();
switch (mode)
{
case 0:printf("%d\n",get(x,y));break;
case 1:link(x,y);break;
case 2:dele(x,y);break;
case 3:date(x,y);break;
}
}
return 0;
}
2018.11.4 update——
萬般借鑑然後瞎搞一天半終於搞出 LCT模板2 (lazy加法乘法下放) 了
這題.....就是多了兩個 lazy 標記
這裡標記和siz差不多維護就好了呀 對於操作就是放到那個點上 更新那個點值 然後標記放放
因此換父子關係的時候就要處理兩子節點 然後更新兩子節點的值
其他照常 對了建圖用link就好了啦
下放程式碼
#include <cstdio>
#define mod 51061
#define MAXN 100010
#define ll long long
using namespace std;
struct splay {
int fa,son[2],siz,rot;
ll ans,v,laz1,laz2;
} e[MAXN];
inline int r()
{
char q = getchar(); int x = 0,y = 0;
while (q < '0' && q != '-' || q > '9') q = getchar();
if (q == '-') ++ y,q = getchar();
while ('0' <= q && q <= '9')
x = (x << 3) + (x << 1) + q - (3 << 4),q = getchar();
return y ? -x : x;
}
inline short pdroot(int x)
{
return e[e[x].fa].son[0] != x && e[e[x].fa].son[1] != x;
}
inline void update(int x)
{
e[x].siz = e[e[x].son[0]].siz + e[e[x].son[1]].siz + 1;
e[x].ans = (e[e[x].son[0]].ans + e[e[x].son[1]].ans + e[x].v) % mod;
}
inline void muldown(int x,ll k)
{
e[x].laz1 = e[x].laz1 * k % mod;
e[x].laz2 = e[x].laz2 * k % mod;
e[x].ans = e[x].ans * k % mod;
e[x].v = e[x].v * k % mod;
}
inline void adddown(int x,ll k)
{
e[x].ans = (e[x].ans + e[x].siz * k) % mod;
e[x].laz1 = (e[x].laz1 + k) % mod;
e[x].v = (e[x].v + k) % mod;
}
inline void rotdown(int x)
{
int bot = e[x].son[0];
e[x].son[0] = e[x].son[1];
e[x].son[1] = bot;
e[x].rot ^= 1;
}
inline void pushdown(int x)
{
if (e[x].laz2 != 1)
{
muldown(e[x].son[0],e[x].laz2);
muldown(e[x].son[1],e[x].laz2);
e[x].laz2 = 1;
}
if (e[x].laz1)
{
adddown(e[x].son[0],e[x].laz1);
adddown(e[x].son[1],e[x].laz1);
e[x].laz1 = 0;
}
if (!e[x].rot) return;
rotdown(e[x].son[0]);
rotdown(e[x].son[1]);
e[x].rot = 0;
}
inline void rotate(int x)
{
int y = e[x].fa,z = e[y].fa,mode = e[y].son[0] == x;
if (!pdroot(y)) e[z].son[e[z].son[1] == y] = x;
e[x].fa = z;
e[y].son[mode ^ 1] = e[x].son[mode];
e[e[x].son[mode]].fa = y;
e[x].son[mode] = y;
e[y].fa = x;
update(y);
update(x);
}
inline void change(int x)
{
if (!pdroot(x)) change(e[x].fa);
pushdown(x);
}
inline void splay(int x)
{
change(x);
while (!pdroot(x))
{
int y = e[x].fa,z = e[y].fa;
if (!pdroot(y)) {
if ((e[z].son[0] == y) ^ (e[y].son[0] == x)) rotate(x);
else rotate(y);
} rotate(x);
}
}
inline void access(int x)
{
for (int y = 0 ; x ; y = x,x = e[x].fa)
{
splay(x);
e[x].son[1] = y;
update(x);
}
}
inline void makeroot(int x)
{
access(x);
splay(x);
rotdown(x);
}
inline void link()
{
int x = r(),y = r();
makeroot(x);
e[x].fa = y;
}
inline void delink()
{
int x = r(),y = r();
makeroot(x);
access(y);
splay(y);
e[x].fa = 0;
e[y].son[0] = 0;
update(y);
link();
}
inline void add()
{
int x = r(),y = r();
makeroot(x);
access(y);
splay(y);
ll k = r();
adddown(y,k);
}
inline void mul()
{
int x = r(),y = r();
makeroot(x);
access(y);
splay(y);
ll k = r();
muldown(y,k);
}
inline void get()
{
int x = r(),y = r();
makeroot(x);
access(y);
splay(y);
printf("%d\n",e[y].ans);
}
int main()
{
int n = r(),q = r(),x,y;
for (int a = 1 ; a <= n ; ++ a)
{
e[a].v = 1;
e[a].siz = 1;
e[a].laz2 = 1;
}
for (int a = 1 ; a < n ; ++ a) link();
char w[1 << 2];
while (q--)
{
scanf("%s",w);
switch (w[0])
{
case '-':delink();break;
case '+':add();break;
case '*':mul();break;
case '/':get();break;
}
}
return 0;
}
搞其他東西去了惹qwq