LCT模板 LCT題型大薈萃
阿新 • • 發佈:2018-12-23
以HDU4010為例,寫了LCT相關的一些題目的做法
#include<stdio.h> #include<iostream> #include<string.h> #include<string> #include<ctype.h> #include<math.h> #include<set> #include<map> #include<vector> #include<queue> #include<bitset> #include<algorithm> #include<time.h> using namespace std; void fre() { freopen("c://test//input.in", "r", stdin); freopen("c://test//output.out", "w", stdout); } #define MS(x,y) memset(x,y,sizeof(x)) #define MC(x,y) memcpy(x,y,sizeof(x)) #define ls o<<1 #define rs o<<1|1 typedef long long LL; typedef unsigned long long UL; typedef unsigned int UI; template <class T1, class T2>inline void gmax(T1 &a, T2 b) { if (b>a)a = b; } template <class T1, class T2>inline void gmin(T1 &a, T2 b) { if (b<a)a = b; } const int N = 3e5 + 10, M = 0, Z = 1e9 + 7, inf = 0x3f3f3f3f; template <class T1, class T2>inline void gadd(T1 &a, T2 b) { a = (a + b) % Z; } int casenum, casei; /* 【演算法介紹】 所謂動態樹(LCT),從資料結構上講,其實只不過是splay的擴充套件。 ACM中的很多問題,都是基於一棵固定形態的樹。然而LCT的存在,使得就算樹形態發生變化,也可以解決很多詢問。 <1> 首先要明確,我們在內部是用splay實現LCT,而在splay中,每個點到根節點距離的期望為log級別,這是LCT複雜度有保障的基礎 <2> 在LCT中我們會有很多條preferred path(最多n條,即每點都獨屬於一條preferred path)。 對於每條preferred path,以深度為排序關鍵字,便可以得到一棵splay(我們把這棵splay稱作auxiliary tree) <3> 我們不需要特殊維護所有點所呈的森林結構,森林結構的所有資訊都存在splay中 splay內部的父子關係是由其位置排列的左右來決定的 splay中的關鍵詞是是深度,所以對於任意一個點x,如果把其轉為其所在splay的根,則ch[x][0]就是x的祖先,ch[x][1]就是x的子孫 即我們需要注意:ch[x][0]並不一定是x的父節點,last(ch[x][0])才是;ch[x][1]並不一定是x的子節點,first(ch[x][1])才是。 同樣,在splay中,fa[x]所指向的節點,並非一定是真實樹形態中x的父節點。 總結一下,就是——在splay中,我們只有通過理清深度關係,才可以明確得到所有點在真實樹形態中的父子關係。 而splay外部,splay的根節點x中存的fa[x],在真實樹結構中,就恰好對應為x節點的父節點。 【演算法實現】 <1> access(x) 該操作的功能是取出從x到真實樹根直接的所有節點(這些節點一個也不多一個也不少,設為集合S)並連結在splay中 在該函式中,每個節點將沿著fa[]指標一直爬到其所在splay的根節點。 由之前的理論可知,在access()中,沿著fa[]的爬升過程中,不一定是在同一棵splay()中進行的,而可能需要連結起不同的splay。 因此每爬升到一個節點x,我們都對x做splay操作,使得ch[x][0]為x的祖先,ch[x][1]為x的子孫,把ch[x][1]丟掉,其都不屬於集合S 同時因為不設計到在ch[x][1]子樹內任何一個節點的修改,所以我們需要使得fa[ch[x][1]]保持不變, 而只是在access的路徑中,連結ch[x][1]為son(son為從起點向上爬升來的暫時的到樹根的鏈)。 我們發現,如果x到root的路徑很長,我們這裡爬升的步數就可能很多,對應一個比較大的時間複雜度,也就是論文中所說的糟糕初勢能。 但是,因為採用了splay壓縮路徑,使得複雜度均攤為log級別。通常,我們在access(x)後再做splay(x)操作,使得x變為根方便後續的處理。 <2> find_root(x) 對於find_root(x),只要先access(x),再順著ch[x][0]向左遞迴,遞迴的終點便是root. <3> make_root(x) 對於make_root(x),只要先access(x),再對x所在整棵splay做reverse,並在翻轉整條鏈之後,把fa[root]賦值給fa[x],使得該splay還具備向其上的splay做轉移的條件。 該操作只會影響(x, root)這條路徑的深度,並且把首端點的延展傳遞。形象而言,就像是一隻蜈蚣,我們抓住其"脊錐",使得(x, root)轉了個頭 <4> 對於link(x, y)操作,我們先make_root(x),把其到實際樹根的整條鏈都取出來,再直接使得fa[x] = y就好了。 <7> 對於cut(x, y)操作,我們先make_root(x),再做access(y)操作,使得提取出了鏈(y, x),把fa[ch[y][0]]和ch[y][0]設為0,即使得y與y的實際父節點刪了邊 <8> 對於modify(x, y, val)操作,我們先make_root(x),再做access(y)操作,使得提取出了鏈(y, x),再add(y, val)即可 <9> 對於query(x, y)操作,我們先make_root(x),再做access(y)操作,使得取出了鏈(y, x),再return 返回關於y的值即可 【要點】 <1> 既然是splay,因為涉及到pushup操作,於是我們必須使得根節點處恰好維護著完全子樹的值,操作的點位也要在根處 同樣的道理,只要是設計從根向下遍歷,就必須做pushdown操作,LCT中的pushdown寫法稍特殊,先找到根再一口氣pushdown,可以減少pushdown的次數。 <2> LCT有時候維護的是有根樹,此時就不需要執行(也不能執行)make_root操作,但是通過access,就能得到該有根樹的祖先。操作在該祖先點上展開即可。 */ int n; pair<int, int>edge[N]; vector<int>a[N]; struct LCT { int ID; int ch[N][2], fa[N]; int v[N]; int mx[N]; bool rv[N]; int ad[N]; void clear(int x) { ch[x][0] = ch[x][1] = fa[x] = 0; rv[x] = ad[x] = 0; } int newnode(int val) { int x = ++ID; clear(x); mx[x] = v[x] = val; return x; } inline int D(int x) { return ch[fa[x]][1] == x; } bool isroot(int x) { return ch[fa[x]][0] != x && ch[fa[x]][1] != x; } void reverse(int x) { if (x == 0)return; swap(ch[x][0], ch[x][1]); rv[x] ^= 1; } void add(int x, int val) { if (x == 0)return; v[x] += val; mx[x] += val; ad[x] += val; } void pushup(int x) { mx[x] = v[x]; gmax(mx[x], max(mx[ch[x][0]], mx[ch[x][1]])); } void pushdown(int x) { if (rv[x]) { reverse(ch[x][0]); reverse(ch[x][1]); rv[x] = 0; } if (ad[x]) { add(ch[x][0], ad[x]); add(ch[x][1], ad[x]); ad[x] = 0; } } void rootdown(int x) { if (!isroot(x))rootdown(fa[x]); pushdown(x); } void setc(int x, int y, int d) { ch[x][d] = y; if (y)fa[y] = x; if (x)pushup(x); } //旋轉操作:旋轉使得節點x與其父節點交換位置 void rotate(int x) { int f = fa[x]; int ff = fa[f]; bool d = D(x); bool dd = D(f); setc(f, ch[x][!d], d); setc(x, f, !d); if (ch[ff][dd] == f)setc(ff, x, dd); else fa[x] = ff; } //旋轉操作:把x旋轉到其所在splay的根節點 void splay(int x) { rootdown(x); while (!isroot(x)) { //if (!isroot(fa[x]))pushdown(fa[fa[x]]); pushdown(fa[x]); pushdown(x); // WHY WA if (!isroot(fa[x]))rotate(D(x) == D(fa[x]) ? fa[x] : x); rotate(x); } } //LCT核心操作:在當前的樹形態下,取出x到root路徑(preferred path)上的所有點,形成auxiliary tree //我們只需要對當前爬升到的點now做splay(now)操作並丟掉其右子樹(子孫),過程中自動把祖先的fa[root]轉移為了fa[now],繼續上跳即可。 void access(int x) { for (int son = 0, now = x; now; son = now, now = fa[now]) { splay(now); setc(now, son, 1); } splay(x); } //LCT基本操作:先找到x所在的auxiliary tree(即preferred path),並查詢返回該條路徑最小的節點(即根) int find_root(int x) { access(x); while (ch[x][0])pushdown(x), x = ch[x][0]; return x; } //LCT基本操作:先找到x所在的auxiliary tree(即preferred path),並把x旋轉為這條路徑的根 void make_root(int x) { access(x); reverse(x); } //LCT基本操作,先使得x為真實樹的根,再提取出y到x這條路徑所表示的splay void getlink(int x, int y) { make_root(x); access(y); } //LCT重要操作:在x與y不在同一棵樹的條件下,把x變為子樹的根,並把x連結為y的子節點 void link(int x, int y) { if (x == y || find_root(x) == find_root(y)) { puts("-1"); return; } make_root(x); fa[x] = y; } //LCT重要操作:在x與y在同一棵樹的條件下,把x變為子樹的根,並把y與y祖先之間的邊徹底斷開 void cut(int x, int y) { if (x == y || find_root(x) != find_root(y)) { puts("-1"); return; } getlink(x, y); ch[y][0] = 0; fa[ch[y][0]] = 0; } //LCT常用操作:在x與y在同一棵樹的條件下,把鏈(x, y)上每個點的權值都+=val void modify(int x, int y, int val) { if (find_root(x) != find_root(y)) { puts("-1"); return; } getlink(x, y); add(y, val); } //LCT常用操作:在x與y在同一棵樹的條件下,詢問鏈(x, y)上的最大點權 int query(int x, int y) { if (find_root(x) != find_root(y))return - 1; getlink(x, y); return mx[y]; } //LCT DEBUG void alldown(int x) { pushdown(x); if (ch[x][0])alldown(ch[x][0]); if (ch[x][1])alldown(ch[x][1]); } void solve() { mx[ID = 0] = -1e9; for (int i = 1; i < n; ++i)scanf("%d%d", &edge[i].first, &edge[i].second); for (int i = 1; i <= n; ++i) { int val; scanf("%d", &val); newnode(val); } for (int i = 1; i < n; ++i)link(edge[i].first, edge[i].second); int q, op, x, y, val; scanf("%d", &q); for (int i = 1; i <= q; ++i) { scanf("%d", &op); if (op == 1) { scanf("%d%d", &x, &y); link(x, y); } else if (op == 2) { scanf("%d%d", &x, &y); cut(x, y); } else if (op == 3) { scanf("%d%d%d", &val, &x, &y); modify(x, y, val); } else { scanf("%d%d", &x, &y); printf("%d\n", query(x, y)); } } puts(""); } }lct; int main() { while(~scanf("%d", &n)) { lct.solve(); } return 0; } /* 論文 —— 《SPOJ375 QTREE 解法的一些研究》 概念學習: <1> 訪問 稱一個點被訪問過,如果剛剛執行了對這個點的access操作 <2> preferred child 如果節點x在子樹中,最後被訪問的節點在子樹y中,且y是x的子節點,那麼我們稱y是x的preferred child 如果最後被訪問的節點就是x本身,那麼它沒有preferred child,每個點到它preferred child之間的邊稱作preferred edge <3> preferred path 整棵樹被劃分成了若干條preferred path,我們對於每條preferred path,用每個點的深度作為關鍵字,用一棵平衡樹來維護。 所以在splay中,每個點左子樹中的點,深度都比其小,意為其祖先,都在preferred path中這個點的上方 每個點右子樹中的點,深度都比其大,意為其子孫,都在preferred path中這個點的下方 <4> Auxiliary Tree 我們使用splay維護preferred path,把樹T分解成若干條preferred path。 我們只需要知道這些路徑之間的連線關係,就可以表示出這棵樹T <5> Link-Cut Tree 將要維護的森林中的每棵樹T表示為若干個Auxiliary Tree,通過Splay中的深度關係,將這些Auxiliary Tree連線起來 的資料結構 HDU4010 [題意] 有一棵n(3e5)個點的樹 每個點有一個權值,權值在任何時候都不會超過int範圍 有Q(3e5)個操作,操作包括4種類型 1 x y:Link操作 要求x與y屬於兩棵不同的樹,此時連線x與y,從而使得兩棵樹合併為一棵 2 x y:Cut操作 要求x與y屬於一棵相同的樹,先使得x變為該樹的根,再切斷y與fa[y]之間的邊,使得一棵樹分裂為兩棵 3 val x y:修改操作 使得x與y之間路徑上所有點權都加val 4 x y:詢問操作 輸出x與y路徑上點權的最大值 [分析] LCT的模板題。與之類似的—— SPOJ OTOCI:涉及到單點修改和鏈上求和,對x做單點修改的時候只要splay(x)即可,並不需要access(x) HDU5333:涉及到維護鏈上次大值,雖然程式碼量大了很多,但是本質都相同 BZOJ2002 [題意] 有1 ~ n共計n(2e5)個點。 對於每個點i,有一個彈跳距離v[i](正整數),如果i + v[i] <= n,則我們在達到 i 點之後會被彈到 i + v[i]點 否則在到達 i 點之後就會被彈飛。有m(1e5)個操作—— 1 x:問你從x出發彈多少次會被彈飛 2 x val:改變v[x]為val [分析] 通常而言,LCT的樹邊是雙向邊,這使得在過程中,我們只要確定好了連結關係,任意一點為樹根其實都不太影響 但是,有些題中LCT的樹邊是單向邊,此時就不能再使用LCT的make_root()函數了,參見此題—— 對於這道題,我們可以把每次彈跳的起點和終點之間連邊。形成有向樹(也是DAG)。於是—— <1> 對於詢問操作,就是問你每個點到其所在樹根之間的邊數。 <2> 對於修改操作,就是涉及到cut與link兩種操作。 需要注意的是,這道題與傳統的LCT不同,邊是有向邊。 但是我們可以把其當做無向邊來看,心裡上明確跳躍過程是從深度較高的點向深度較低的點進行的就好。 可是在LCT中,我們經常會做make_root(x)這樣之類的操作,這實際中是不允許的。 本題中需要實現的函式是cut()和link(),其中—— cut(x, y)的功能是把x與y之間的邊刪掉,此時我們需要access(x),取出x所在的整條splay(即從x到root的路徑) 然後使得x與其祖先的關聯性完全斷開,即執行ch[x][0] = fa[ch[x][0]] = 0操作 link(x, y)的功能是把x與y之間連邊,因為每個點的出度都最多為1. 所以此時顯然x應當是其所在splay的祖先,我們只需要splay(x),fa[x] = y即可(access(x)實際也一樣)。 HDU5967 2016年中國大學生程式設計競賽(合肥)小R與手機 [題意] n(2e5)個點,每個點x最多一個出度指向v[x]。 告訴你初始的連結關係,如果v[x] == 0表示無出度 m(2e5)個操作—— 1 x y:使得x指向y 2 x:詢問如果從x出發,是否能走到一個終止節點,有則輸出,無則輸出-1 [分析] 問題還是先分析好再做得好。 這道題的動態連結關係顯然可以通過LCT實現。 然而,這個連結關係可能會形成環,而LCT顯然是不支援環結構的。怎麼辦? <1>無環,無環的話,從每個點沿著有向邊出發,最終找到的節點,這個節點指向0即可 注意,明確對應關係的話,問題會更好做,要知道——這個點就是根節點! <2>有環,有環的話,其實我們需要使得一個節點的指向關係儲存卻不生效。 想想看,這個節點,其實也只能是根節點。 得到之前的結論,問題便變得清晰一些了—— 1. 我們只要生效所有連結關係,比如此時要連結(x, y),x是還沒有生效連結關係的,但是發現find_root(y) == x,那便發現了環。 於是我們在物理結構上,x依然是access()後能得到的根(之一),但同時保留了v[x],有v[x]不為0。即根的v[]不為0,也就對應著是存在了環 2. 我們過程中還要繼續生效鏈關係,比如此時要連結(x, y)。但是—— 對於x,其之前可能也存在著連結的目標z 如果x是根,(x,z)的連結關係可能未生效,我們直接相對於對x做嶄新的連結即可 如果(x,z)的連結關係已經生效,首先我們一定要斷開(x,z)的連結關係,然後對於z所在的tree的root,如果其root和v[root]之間斷邊了,則修改v[root] 然後的連結操作與之前無異,是傻瓜式的。 於是link這麼寫—— void link(int x, int y) { int w = find_root(x);//先找到根節點 if (v[x] && x != w)cut(x, v[x]);//有邊且不是根節點,就刪邊 v[x] = y;//標記 if (y && x != find_root(y))splay(x), fa[x] = v[x];//根據是否有環決定是否加邊,splay(x)而不是access(x)是因為x顯然是根,其實都一樣 if (v[w] && x != w && w != find_root(v[w]))splay(w), fa[w] = v[w];//根據是否有環決定是否恢復邊,splay(w)是因為w顯然是根 //注意,這裡我們是有判斷x != find_root(y)和 w != find_root(v[w]),左側之所以不用find_root(x)和find_root(w), //是因為左側的點必然是根,而如果x == w,則顯然x因為指向了y,x就不再是根,即導致最後一句話出錯,於是加了條件x != w //其實可以不加這些判定條件,而把x換成find_root(x),把w換成find_root(w)來解決問題。 } cut這麼寫——(其實這裡的cut不需要引數y) void cut(int x, int y) { //if (x == y || find_root(x) != find_root(y)) { puts("-1"); return; } access(x); ch[x][0] = fa[ch[x][0]] = 0;//rgt -> lft } HDU5333 LCT動態樹 離線詢問+樹狀陣列(本題與HDU4677題意和做法完全相同,只是資料範圍卡住了分塊做法) [題意] 給你一個n(1e5)點和m(2e5)邊的無向圖。 同時存在q(1e5)個詢問,對於每個詢問,有區間[L, R] 我們只連線兩個端點都在[L, R]中的所有邊,則該圖會形成多少個連通塊。 [分析] 顯然,[1, L - 1] 與 [R + 1, n]中的點都沒有任何邊連線。 於是,答案其實是:(L - 1 + n - R) + [L, R]區間內起到實質效應的邊數。 首先這道題詢問眾多,我們考慮離線化所有詢問。 比如,我們按照右界從小到大的順序,對所有邊做排序,就可以只加入右界不超過R的所有邊。 然而,這樣子形成的圖並不一定是樹,可能形成了圖,這使得我們無法動態維護splay。 於是,我們在加邊的過程中也要動態維護其森林結構。 具體的做法是使用貪心,對於每次新加的邊(不考慮自環和重邊),如果加入該邊會形成環,那麼, 對於加邊(x, y)之間的鏈,和該邊共成的環,我們刪掉環上的任意一條邊,整個圖的連通性都是一樣的。 這裡基於後效性最優的貪心原則,我們只要刪掉該環上左端點最小的邊,該邊的用途是最小的。 於是,我們把邊抽象為點,原始點的邊權為極大,邊點的權值為該邊所對應的較小節點的編號,於是LCT的查詢是樹鏈上的最小節點編號。 因為右邊界為R的詢問也有很多,對應很多不同的L,我們用樹狀陣列維護此時有從L ~ n的邊加了多少條。 每多一條邊便少一個連通塊,由此更新答案。 void solve() { v[ID = 0] = inf; for (int i = 1; i <= n; ++i) newnode(inf), bit.v[i] = 0; for (int i = 1; i <= m; ++i) { scanf("%d%d", &edge[i].l, &edge[i].r); if (edge[i].r < edge[i].l)swap(edge[i].l, edge[i].r); } for (int i = 1; i <= Q; ++i) { scanf("%d%d", &q[i].l, &q[i].r); q[i].id = i; } sort(edge + 1, edge + m + 1); sort(q + 1, q + Q + 1); for (int i = 1, p = 1; i <= Q; ++i) { while (p <= m && edge[p].r <= q[i].r) { int x = edge[p].l; int y = edge[p++].r; int o = newnode(x); if (x == y || x == edge[p - 2].l && y == edge[p - 2].r)continue; if (find_root(x) == find_root(y)) { getlink(x, y); int u = mp[y]; if (v[u] >= x)continue; cut(u, edge[u - n].l); cut(u, edge[u - n].r); bit.modify(v[u], -1); } link(x, o); link(y, o); bit.modify(x, 1); } ans[q[i].id] = n - bit.check(q[i].l); } for (int i = 1; i <= Q; ++i)printf("%d\n", ans[i]); } 2014-2015 ACM-ICPC(CERC 14) J LCT動態樹 + 可持久化線段樹 Pork barrel [題意] n(1000)個點 m(100000)條邊 q(1e6)個詢問 對於每個詢問[L,R],問你,如果使用的邊的邊權在[L,R]範圍內,那麼—— 我們能夠得到的最小邊權的極大生成森林的邊權之和是多少,強制線上。 [分析] 如果可以離線化的話,我們直接把邊按照權值從大到小做排序,也把詢問按照其左界從大到小做排序. 然後列舉詢問,詢問的左界遞減,我們加的邊數也越多。 對於一條邊,如果其加入成環了,我們會刪掉其所在環上邊權最大的一條邊。 然後對於查詢[L,R],如果有能夠起到同樣功效的大邊,會被刪掉,如果有沒有能夠起到相同功效的大邊,我們也保留了適當的小邊。 也就是說,在樹狀陣列中動態維護邊權,check(r)就是答案。 然而,這道題需要使得詢問線上,於是,如果我們還想要使用離線的方法的話,就要記錄下所有可能詢問的答案。 該做法是使用可持久化線段樹(或者可持久化樹狀陣列),對於每一個左界我們都維護一棵線段樹,最後查詢就好啦—— void solve() { scanf("%d%d", &n, &m); v[ID = 0] = -inf; for (int i = 1; i <= n; ++i) newnode(-inf); topval = 0; for (int i = 1; i <= m; ++i) { scanf("%d%d%d", &edge[i].x, &edge[i].y, &edge[i].v); rk[++topval] = edge[i].v; } sort(rk + 1, rk + topval + 1); topval = unique(rk + 1, rk + topval + 1) - rk - 1; sort(edge + 1, edge + m + 1); pst.sz = 0; pst.rt[topval + 1] = 0; for(int l = topval, p = 1; l >= 1; --l) { pst.rt[l] = pst.rt[l + 1]; while (p <= m && edge[p].v == rk[l]) { int x = edge[p].x; int y = edge[p].y; int o = newnode(edge[p++].v); if (find_root(x) == find_root(y)) { getlink(x, y); int u = mp[y]; if (v[u] <= v[o])continue; cut(u, edge[u - n].x); cut(u, edge[u - n].y); pst.addp(pst.rt[l], 1, topval, lower_bound(rk + 1, rk + topval + 1, v[u]) - rk, -v[u]); } pst.addp(pst.rt[l], 1, topval, l, v[o]); link(x, o); link(y, o); } } scanf("%d", &Q); int ans = 0; for (int i = 1; i <= Q; ++i) { int l, r; scanf("%d%d", &l, &r); l = lower_bound(rk + 1, rk + topval + 1, l - ans) - rk; r = upper_bound(rk + 1, rk + topval + 1, r - ans) - rk - 1; if (l > r)ans = 0; else ans = pst.check(pst.rt[l], 1, topval, l, r); printf("%d\n", ans); } } */