<style> .reveal .slides { text-align: left; font-size:30px } </style> ## 高維偏序問題 & 持久化資料結構 ---- - CDQ分治 - 二維資節 - 2D BIT - 2D 線段樹 - 持久化 - 持久化線段樹 - K-th Number - 樹上持久化 - 可復原並查集 - 時間線段樹 --- ## CDQ 分治 IOI 2008 金牌得主陈丹琦 (Dan-qi Chen)提出 用來解決偏序問題,加入資料結構後(BIT)也可以解決三維偏序問題 ---- ## 一維偏序問題 給集合 $s$,對於集合內的每個元素 $a$, 有多少元素 $b\in s$ 小於 $a$ ---- ## 二維偏序問題 給二維空間上 $n$ 個點 $(x_i, y_i)$ 求出有多少點對符合 $i\neq j$, $x_i < x_j$, $y_i < y_j$ ---- 會發現這個問題和逆序數對問題很像 逆序數對為給定一個序列 $a$,求有多少數對符合 $i < j$ 且 $a_i > a_j$ ---- 把 $n$ 個點 $(x_i, y_i)$ 先照 $x_i$ 排好後, 就和逆敘數對的序列的 index 一樣是升序的 接下來的問題就和逆序數對相似 ---- 要解決二維偏序問題 先將第一個維度排序後,接下來有以下幾種統計第二個維度的方法 : 1. merge sort 2. BIT 等資料結構 而二維的 CDQ 分治,他的實作代碼和 merge sort 差不多, 也是遞迴 $[l,mid]$ 和 $[mid+1,r]$ 去解決子問題後, 計算左區間對右區間的影響(貢獻) ---- 遞迴 $[l,mid]$ 和 $[mid+1,r]$ 去解決分別的子問題 計算左區間對右區間的影響(貢獻) ```cpp= void cdq(int l, int r){ if(l == r) return; int mid = (l+r)/2; cdq(l, mid); cdq(mid+1, r); merge(l, r); } ``` ---- merge sort 的過程中,計算左區間對右區間的貢獻 已知第一個維度 $x$ 已經排序好了,也就是保證左區間 $x\le$ 右區間的 $x$ ![image](https://hackmd.io/_uploads/ry6lp-5sA.png) ---- 而第二個維度 $y$ 可以分別在子區間排序, 對於右區間的每個元素,計算左區間有多少元素的 $y$ 小於自己的 $y$ ![image](https://hackmd.io/_uploads/rJhZp-9i0.png) 做完後順便用雙指針照 $y$ 合併排序 ---- merge sort 的過程中,順便計算左區間對右區間的貢獻 ```cpp= int x[MXN], y[MXN], tmp[MXN]; vector<int> ord; void cdq(int l, int r){ if (l == r) return; int mid = (l+r)/2, i = l, j = mid+1, tid=0; cdq(l, mid); cdq(mid+1, r); for (; j <= r; ++j){ while (i <= mid && y[ord[i]] <= y[ord[j]]) tmp[tid++] = ord[i++]; ans += i - l; tmp[tid++] = ord[j]; } while(i <= mid) tmp[tid++] = ord[i++]; copy(tmp, tmp+r-l+1, ord.begin() + l); } int main(){ cin >> n; for(int i = 0; i < n; i++) cin >> x[i]; for(int i = 0; i < n; i++) cin >> y[i]; ord.resize(n); iota(ord.begin(), ord.end(), 0); sort(ord.begin(), ord.end(), [&](int lhs, int rhs){ if(x[lhs] != x[rhs]) return x[lhs] < x[rhs]; return y[lhs] < y[rhs]; }); cdq(0, n-1); cout << ans << endl; } ``` ---- ## 三維偏序問題 求 $x_i < x_j, y_i < y_j, z_i < z_j, i\neq j$ 的數量 ---- 一樣遞迴 $[l,mid]$ 和 $[mid+1,r]$ 去解決子問題後, 計算左區間 $[l,mid]$ 對右區間 $[mid+1,r]$ 的影響(貢獻) ---- 照第一維 sort 後,左區間的 $x\le$ 右區間的 $x$ 第二維左右區間各自照 y 排序,把 z 用資料結構維護 ---- 使用資料結構維護 對於左區間在 $z_l$ 加值 右區間的點為詢問左邊有幾個點 $z_l < z_r$ ---- 每輪做完後要把 BIT 清空 記得不能直接跑過 BIT 的每個位置清空 要看左區間用到哪些位置就清空那些就好 ---- 總複雜度遞迴 $\log N$ 層 每層 $O(N\log N)$ 維護 總複雜度 $O(N\log^2 N)$ ---- 只要能把題目轉換成偏序問題,就可以用 cdq 分治解掉。 ---- ```cpp= int CDQ (int l, int r) { if (l == r) return; int mid = (l + r)/2; CDQ(l, mid), CDQ(mid+1, r); vector<int> tmp; for (int i = l, j = mid+1; i <= mid or j <= r; ) { while (i < mid and (j == r or y[ord[i]] <= y[ord[j]])) { bit.add(z[ord[i]], 1); tmp.push_back(ord[i]); i++; } if (j <= r) { ans[ord[j]] += bit.que(z[ord[j]]); tmp.push_back(ord[j]); j++; } } for (int i = l; i <= mid; i++) bit.add(z[ord[i]], -1); copy(tmp.begin(), tmp.end(), ord.begin() + l); }; ``` --- ## 二維資料結構 - 2D BIT - 2D Segment Tree ---- ## 經典題 2D 版 給 $n\times m$ 的矩陣,$q$ 筆操作,每次為以下其中一種 1. 左上 $(l, u)$ 到右下 $(r, d)$ 的子矩陣,每個元素加值 $v$ 2. 詢問左上 $(l, u)$ 到右下 $(r, d)$ 的子矩陣總和 <br><br> - $1\le n, m\le 5000$ - $1\le q\le 10^5$ ---- 開 $n$ 顆線段樹分別維護長度為 $m$ 的區間 ? <span>$\to$ $O(q\cdot n \log m)$<br><!-- .element: class="fragment" data-fragment-index="1" --></span> <span>$\to$ $6\cdot 10^9$<br><!-- .element: class="fragment" data-fragment-index="2" --></span> <span>$\to$ TLE :\(<br><!-- .element: class="fragment" data-fragment-index="3" --></span> ---- $\to$ 2D Data Structure ! ---- ## 2D Binary Indexed Tree ---- 對於 1D BIT,第 $i$ 個位置維護的是區間 $(i-lowbit(i), i]$ 的值。 query(i) 求的是 $1\sim i$ 的前綴總和 ```cpp= ll query(int x){ ll ret = 0; while(x){ ret += bit[x]; x -= lowbit(x); } return ret; } ``` ---- ## 求出前綴總和 ![](https://i.imgur.com/yFuFw9m.png =500x) 求出前綴區間 a[1..11] 從 11 開始, 11 儲存區間長度為 1$\to$ a[11..11] 得到 $a_{11}$,往前求出位置 10 儲存的總和 $\to$ a[9..10] 得到 $a_{[9..10]}$,往前求出位置 8 儲存的總和 $\to$ a[1..8] prefix$_{11} = a[1..8] + a[9..10] + a[11..11]$ ---- 而 2D BIT,第 (i, j) 個位置維護的是區間 (i-lowbit(i), i]$\times$ (j-lowbit(j), j] 的值 並且 query(x, y) 得到的是左上到 (x, y) 的二維前綴區間總和 ---- ## query(x, y) 透過加總 BIT[x][y] + BIT[x][y-lowbit(y)] + BIT[x][(y-lowbit(y)) - lowbit(y-lowbit(y))] +... ```cpp= ll tot = 0; while(y){ tot += BIT[x][y]; y -= lowbit(y); } cout << tot << endl; ``` 可以得到區間 (x-lowbit(x), x] $\times$ [1, y] 的區間總和 ---- ## query(x, y) 使用兩個迴圈遍歷兩個維度,不斷 -lowbit(x) 跳到下一個區間,總共 $\log^2 n$ 個區間 可以得到 $[1, x]\times [1, y]$ 的區間總和 單筆操作複雜度 $O(\log n\log m)$ ---- ```cpp= ll query(int x, int y){ ll ret = 0; for(int i = x; i; i -= lowbit(i)){ for(int j = y; j; j -= lowbit(j)){ ret += BIT[i][j]; } } return ret; } ``` ---- ## update(x, y) 同理,每次使用 +lowbit(x) 遍歷到下一個區間更新 總共 $O(\log n\log m)$ 個區間 ---- ```cpp= void update(int x, int y, int v){ for(int i = x; i <= n; i+=lowbit(i)){ for(int j = y; j <= m; j+=lowbit(j)){ BIT[i][j] += v; } } } ``` ---- 要得到區間 (l, u) ~ (r, d) 的總和, 使用排容 query(r, d) - query(r, u-1) - query(l-1, d) + query(l-1, u-1) 可以得到 ---- $O(q\cdot \log n\log m$) $\to$ $1.5\times 10^7$ ---- ## 二維線段樹 對於二維總和問題使用 2D BIT 可以做到, 但如果把詢問區間總和換成詢問區間最大值則需要換線段樹處理。 ---- 為了方便解說二維線段樹, 接下來的例子一樣使用求 2D 區間總和。 ---- ## 作法 線段樹裡再開線段樹,也就是第一層的線段樹的每個節點都是一棵線段樹 第一層放第一個維度 $r$,第二層放第二個維度 $c$ ---- 遞迴時先遞迴第一個維度 $r$(第一層線段樹), 每次詢問/修改先遞迴到第一個維度的區間, 接著遞迴第二個維度 $c$ (第二層線段樹),在第二層遞迴到指定的區間求解 ---- ![](https://hackmd.io/_uploads/SyBUIrWT2.png =600x) figure modify from https://usaco.guide/CPH.pdf#page=274 ---- ### 功能 - 2維區間詢問 - 單點修改 ### 函數 ```cpp void build(idx, idy, lx, ry, lx, ry); void update(idx, idy, lx, ry, lx, ry, pos, val); int query(idx, idy, lx, ry, lx, ry); ``` ### 線段樹大小 $16 \cdot N\cdot M$ ---- ## build 先遞迴第一個維度 $x$,再對於每個 $x$ 的區間, 遞迴第二層 $y$ 的區間,算出第二層每個子區間的總和 ---- ```cpp= #define cl(x) (x<<1)+1 #define cr(x) (x<<1)+2 void build_y(int idx, int idy, int lx, int rx, int ly, int ry){ if(ly == ry){ tree[idx][idy] = arr[lx][ly]; return; } int my = (ly+ry)/2; build_y(idx, cl(idy), lx, rx, ly, my); build_y(idx, cr(idy), lx, rx, my+1, ry); tree[idx][idy] = tree[idx][cl(idy)] + tree[idx][cr(idy)]; } void build_x(int idx, int lx, int rx){ if(lx != rx){ int mx = (l+r)/2; build_x(cl(idx), lx, mx); build_x(cr(idx), mx+1, rx); } build_y(idx, 0, lx, rx, 0, m-1); } ``` ---- ## query 詢問區間 $(qxl, qyl)\sim (qxr, qyr)$ 的區間和 一樣先遞迴第一個維度 $x$,遞迴到 $x$ 符合的詢問區間再遞迴 $y$ 的詢問區間 ```cpp= int query_y(int idx, int idy, int ly, int ry, int qly, int qry){ if(qly <= ly && ry <= qry){ return tree[idx][idy]; } int my = (ly+ry)/2, ret = 0; if(qy <= my) ret += query_y(idx, cl(idy), ly, my, qly, qry); if(qy > my) ret += query_y(idx, cr(idy), my+1, ry, qly, qry); return ret; } int query_x(int idx, int lx, int rx, int qlx, int qly, int qrx, int qry){ if(qlx <= lx && rx <= qrx){ return query_y(idx, 0, 0, m-1, qly, qry); } int mx = (lx + rx)/2, ret = 0; if(qlx <= mx) ret += query_x(cl(idx), lx, mx, qlx, qly, qrx, qry); if(qrx > mx) ret += query_x(cr(idx), mx+1, qrx, qlx, qly, qrx, qry); return ret; } ``` ---- ## update 更新 a[x][y] = p 所有有被遞迴到的區間都有子區間被更新 因此有遞迴到的區間都繼續遞迴下一個維度 ```cpp= void update_y(int idx, int idy, int lx, int rx, int ly, int ry, int qy, int val){ if(ly == ry){ if(lx == rx) tree[idx][idy] = val; else tree[idx][idy] = tree[cl(idx)][idy] + tree[cr(idx)][idy]; return; } int my = (ly+ry)/2; if(qy <= my) update_y(idx, cl(idy), lx, rx, ly, my, qy, val); else update_y(idx, cr(idy), lx, rx, my+1, ry, qy, val); tree[idx][idy] = tree[idx][cl(idy)] + tree[idx][cr(idy)]; } void update_x(int idx, int lx, int rx, int qx, int qy, int val){ if(lx != rx){ int mx = (lx + rx)/2; if(qx <= mx) update_x(cl(idx), lx, mx, qx, qy, val); else update_x(cr(idx), mx+1, rx, qx, qy, val); } update_y(idx, 0, lx, rx, 0, m-1, qy, val); } ``` ---- ## 2D線段樹複雜度 單筆操作 $O(\log N\log M)$ 空間複雜度 $O(16NM)$ 會發現空間複雜度非常高,因此通常第二個維度都是用動態開點去實作 以節省複雜度 ### d 維複雜度 單筆操作時間複雜度 $O(\log^dN)$ 空間複雜度 $O((4N)^d)$ ---- 對於二維的資料結構問題,如果只維護 $n$ 個一維資料結構複雜度可以過,則不需要毒瘤使用二維資節。 雖然複雜度較好,但實作較複雜容易出錯。 --- ## 可持久化資料結構 可持久化資料結構(Persistent data structure)是一種能夠在修改之後其保留歷史版本(即可以在保留原來數據的基礎上進行修改 --- 比如增添、刪除、賦值)的資料結構。這種資料結構實際上是不可變對象,因為相關操作不會直接修改被保存的數據,而是會在原版本上產生一個新分支 --wikipedia ---- - 可持久化線段樹 - 可復原並查集 --- ## 可持久化線段樹 由於引入者黃嘉泰姓名的縮寫與前中共中央總書記、國家主席胡錦濤(H.J.T.)相同,因此這種資料結構也被稱為「主席樹」 ---- ### 功能 修改值,詢問值,回復歷史版本 如以下操作 1. **單點**修改第 $i$ 格的值為 $v$ 2. 詢問區間 $[l,r]$ 的總和為多少 3. 將序列變回第 $k$ 次操作之前的版本 ---- 當有數值修改,如果每次都重新把整棵線段樹重新存一遍, 則會花費過多的空間與時間 操作 $k$ 次,則有 $k$ 個版本,複雜度為 $O(nk)$ ---- 觀察下面的圖會發現,如果修改一個點的值,最多只會更新 $\log N$ 個點 其他點不影響,所以其實如果有修改只需要儲存那 $\log N$ 個點就好 <div style="position: absolute; right: 60%; top:100%;"> ![](https://i.imgur.com/EBZfqkG.png =400x) </div> <div style="position: absolute; left: 57%; top:100%;"> ![](https://i.imgur.com/P88vk6i.png =400x) </div> ---- 如下圖為例,一樣修改第 3 格的值,則將有變動到的節點重新存一遍,沒變動到的區間直接指向原本的地方 <div style="position: absolute; right: 60%; top:100%;"> ![](https://i.imgur.com/EBZfqkG.png =400x) </div> <div style="position: absolute; left: 57%; top:100%;"> ![](https://i.imgur.com/A0OkJXy.png =400x) </div> ---- 原本線段樹的節點使用 index 來找左右子節點, 但持久化後每個版本的節點都會不同 因此我們可以改用指標來實作 ```cpp= struct node{ ll val; node *l, *r; }; ``` ---- 而每次更新版本時,我們需要再儲存新版本的根節點位置, 方便之後回復版本時找到那棵線段樹 ![](https://i.imgur.com/A0OkJXy.png =400x) ```cpp= void add_ver(int x,int v){ //修改位置 x 的值為 v ver.push_back(update_ver(ver.back(), 0, n-1, x, v)); } ``` ---- ### 實作細節 使用指標實作 ```cpp= struct node{ ll val; node *l, *r; }; vector<node *> ver; //用一個vector紀錄全部版本的根節點 void build(node *now_ver, l, r); ll query(node *now_ver, l, r, ql, qr); node *update_ver(node *pre_ver,int l,int r,int pos,int v); //回傳新建的節點 void add_ver(int x,int v){ //修改位置 x 的值為 v ver.push_back(update_ver(ver.back(), 0, n-1, x, v)); } ``` ---- ### 建新版本 ```cpp= node *update_ver(node *pre_ver, node *x, int l, int r, int pos, int v){ node *x = new node(); //當前位置建立新節點 if(l == r){ x->val = v; return x; } int mid = (l+r)>>1; if(pos <= mid){ //更新左邊 x->l = update(pre_ver->l, x->l, l, mid, pos, v); //左邊節點連向新節點 x->r = pre_ver->r; //右邊連到原本的右邊 } else{ //更新右邊 x->l = pre_ver->l; //左邊連到原本的左邊 x->r = update(pre_ver->r, x->r, mid+1, r, pos, v); //右邊節點連向新節點 } x->val = x->l->val + x->r->val; return x; } ``` ---- ### 用途 上面的舉例為裸的操作, 而持久化線段樹還有很大的用途 通常是用來解決 **兩個維度** 以上的較複雜的問題。 因此通常需要自己分析題目所需,轉換為持久化線段樹實作。 ---- ### 例題 給定長度為 $n$ 的序列 $a$, $q$ 筆詢問,每次問 **區間 [l, r]** 之間,第 **$k$** 大的數字 $1 \le n, q \le 2 \cdot 10^5$ $1 \le a_i \le 10^9$ <span>$\to$ 區間 [l, r]<!-- .element: class="fragment" data-fragment-index="1" --></span> <span>$\to$ 第 k 大<br><!-- .element: class="fragment" data-fragment-index="2" --></span> <span>兩個維度的問題 !<br><!-- .element: class="fragment" data-fragment-index="3" --></span> ---- ## 暴力的作法 每筆詢問使用 nth_element 花 O(n) 時間找第 k 大, 總複雜度 $O(nq)$ <span>會發現這個複雜度好像似曾相似? <!-- .element: class="fragment" data-fragment-index="1" --></span> <span>持久化線段樹的暴力版 O(nk)! <!-- .element: class="fragment" data-fragment-index="2" --></span> ---- 把一個其中一個維度使用持久化的概念 ! - 區間 [l, r] - 第 k 大 ---- ### 區間 [l, r] 使用持久化處理 從第一個數字開始依序加入進線段樹 每更新一個數字,當成多一個版本 線段樹內的每個節點儲存區間 [l, r] 內的數字出現數量 第 $i$ 棵線段樹儲存前綴 $i$ 個元素 ---- ### 第 k 大 要求出線段樹上第 $k$ 小的數字, 可以在線段樹上二分搜。 ```cpp= //val 儲存當前節點的數量 int query(node *x, int l, int r, int k){ if(l == r) return l; int mid = (l+r)>>1; if(x->l->val <= k) return query(x->l, l, mid, k); else return query(x->l, mid+1, r, k - x->l->val); } ``` ---- ### 區間第 k 大 而使用第 $r$ 個版本的數量 - 第 $l-1$ 個版本求出來的數量 即可求出區間 [l, r] 內第 $k$ 小的數字 ```cpp= // pre 為第 l-1 個版本, now 為第 r 個版本 int query(node *pre, node *now, int l, int r, int k){ if(l == r) return l; int mid = (l+r)>>1; int l_num = now->l->val - pre->l->val; if(k <= l_num) return query(pre->l, now->l, l, mid, k); else return query(pre->l, now->l, mid+1, r, k - l_num); } ``` ---- 由於數字範圍很大,記得要先離散化 總複雜度 $O(q\log n)$ ---- ### 樹上求路徑第 k 大 給一棵樹,每個點有點權 q 筆詢問,每次給你 `u v k`,求出點 u 到點 v 的路徑中,權重第 k 大的點權是多少? ---- 可能會想到 LCA ? 還有呢? ---- 序列上求區間第 k 大是第 r 個版本 - 第 l 個版本的數量去求第 k 大 而樹上的問題選一個點當根,當前版本由父節點轉移過來,記錄從祖先到自己總共有那些數字 ---- 如此一來,要求 u 到 v 路徑的數量,等價於 (u 到 lca) + (v 到 lca) - (lca) 的數量就能轉換成相似的問題去做了。 --- ## 可復原並查集 ### Rollback DSU 可復原並查集支援以下功能 1. 查詢元素所在集合 2. 合併兩個集合 3. 回復上一步合併操作 ---- 1. 查詢元素所在集合 2. 合併兩個集合 前兩個操作就是一般的並查集 3. 回復上一步合併操作 第三個操作要如何回復? ---- ### 使用啟發式合併維護 由於路徑壓縮會破壞節點之間的關係, 因此維護並查集時只使用啟發式合併維護,不用路徑壓縮 ```cpp= int f[MXN], sz[MXN]; int find(int x){ return f[x] == x ? x : find(f[x]); } void merge(int x, int y){ x = find(x); y = find(y); if(x == y) return; if(sz[x] > sz[y]){ sz[x] += sz[y]; f[y] = x; } else{ sz[y] += sz[x]; f[x] = y; } } ``` ---- ### 紀錄每次合併前的狀態 可以觀察到每次合併時,改變的為 sz[x], f[y] 因此記錄起來,需要回覆上一個合併操作時,直接設為原本的值即可 ```cpp= vector<tuple<int, int, int, int>> ver; void merge(int x, int y){ x = find(x); y = find(y); if(sz[x] < sz[y]) swap(x, y); ver.push_back(make_tuple(x, sz[x], y, f[y])); if(x != y){ sz[x] += sz[y]; f[y] = x; } } ``` ---- ### Undo 回覆上一個合併操作時,直接設為原本的值即可 ```cpp= void undo(){ auto [x, szx, y, fy] = ver.back(); ver.pop_back(); sz[x] = szx; f[y] = fy; } ``` ---- 由於使用啟發式合併維護並查集,複雜度為 $O(\log n)$ 而記錄版本與回復的時間複雜度為 $O(1)$ 總複雜度為 $O(q\log n)$ ---- ### 時間線段樹 穿越時空來解決動態連通性的問題 ---- ### 經典例題 --- [Dynamic Connectivity](https://cses.fi/problemset/task/2133) 給定一張 $N$ 個點的無向圖,一開始圖上有 $M$ 條邊。 現在有 $Q$ 個操作,每個操作會是以下兩種中的一種: 1. 增加一條連接著編號 $A_i$ 與編號 $B_i$ 的邊。 2. 刪除一條連接著編號 $A_i$ 與編號 $B_i$ 的邊(保證這條邊是存在的)。 每次操作完後請輸出當前的連通塊數量。 - $1 \le N, Q \le 10^5$ - $1 \le A_i, B_i \le N$ ---- 如果只有第一個操作 1. 增加一條連接著編號 $A_i$ 與編號 $B_i$ 的邊。 每次操作完後請輸出當前的連通塊數量。 ---- 使用一般並查集好好維護連通塊大小即可 ---- 如果只有第二個操作 2. 刪除一條連接著編號 $A_i$ 與編號 $B_i$ 的邊(保證這條邊是存在的)。 每次操作完後請輸出當前的連通塊數量。 ---- 先把最後的圖儲存起來 可以反著從最後一個刪除操作做回來,就變成只有加邊的問題了 ---- 而原問題...加邊/刪邊同時... <span>還是不會做 :P <!-- .element: class="fragment" data-fragment-index="1" --></span> ---- ### 時間線段樹 ! 離線作法 把每條邊的生存時間當成區間儲存在線段樹上。 ---- 假設依序有 8 個事件,把每個事件當成一個時間點 1. add 1 3 2. add 2 5 3. query 3 4. remove 1 3 5. add 4 2 6. query 2 7. remove 4 2 8. add 1 2 1 3 這條邊的生存時間為時間 1~4 2 5 這條邊的生存時間為時間 2~8 4 2 這條邊的生存時間為時間 5~7 1 2 這條邊的生存時間為時間 8~8 ---- 把邊放到線段樹上 1 3(1~4), 2 5(2~8), 4 2(5~7), 1 2(8~8) ![](https://hackmd.io/_uploads/BkTNwNGT3.png) ---- 把每條邊(event) 依序加入到線段樹上儲存 ```cpp= vector<pair<int, int>> tree[MXN<<2]; void insert(int i, int l, int r, int ql, int qr, pair<int, int> event){ if(ql <= l && r <= qr){ tree[i].push_back(event); return; } int mid = (l+r)>>1; if(ql <= mid) insert(cl(i), l, mid, ql, qr, event); if(qr > mid) insert(cr(i), mid+1, r, ql, qr, event); } ``` ---- 接著只需要遍歷這顆線段樹! 每走到一個節點時,把當前節點上的邊使用並查集合併 往下遞迴子節點,走到一個詢問時儲存連通塊大小到答案中 離開當前節點時,把當前節點上的邊回復 ---- 走到最深時,判斷當前時間是否有詢問,有的話將答案儲存起來 ```cpp= void traversal(int i, int l, int r){ if(l == r){ if(query[l]) ans[query[l]] = component_num; return; } for(pair<int, int> event : tree[i]) merge(event.first, event.second); int mid = (l+r)>>1; traversal(cl(i), l, mid); traversal(cr(i), mid+1, r); for(int i = 0; i < tree[i].size(); i++) undo(); } ``` ---- ### 複雜度 - 插入一個事件進線段樹上複雜度為 $O(\log n)$ - 合併為 $O(\log n)$ - 回復為 $O(1)$ - 遍歷線段樹為 $O(n+q)$ 總複雜度 $O(q\log n)$
{"title":"High-Dimensional Problem and Persistent DS","contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":17050,\"del\":922}]","description":"CDQ分治"}
    434 views
   Owned this note