--- tags: 進階班 --- # 線段樹 Segment tree 2 ## 本次範圍 懶標記、動態開點 ## 前言 上次說過懶標記可以**區間修改**,這次就來介紹! 而離散化技巧可以有效壓縮線段樹的大小,有時候是必要手段 ## 懶標記 如果使用單點修改的方式做區間修改,時間複雜度為 $O(KlgN)\Rightarrow$ 超慢 ($K = r - l$ ) 對於區間修改,以一個題目來說,有時我們修改一個區間,但後來題目並不會詢問到那一個區間, 那麼,~~幹嘛改阿~~ 這時,我們可以利用「標記」 `tag[i]` 來紀錄哪些是「待改」的線段。 流程如下: #### 修改時 1. 在修改時,先把欲修改的值加入 `tag[i]` 2. **把「欲修改的線段」的父線段及本身線段先修改** 要先修改父線段和自己的原因是因為 `query()` 時,可能只會搜尋到大線段, 如果小線段的修改值沒有傳給大線段,那麼就會 `NA`。 3. 如果目前遍歷到的線段,其 `tag[i]` 值不為 $0$,則也要修改線段。 (可能是前面的修改遺留下來的) 因此,`modify()` 會變成: ```cpp= void modify(int ql, int qr, LL val, int l = 0, int r = n, int i = 1){ /*修改線段*/ if(qr <= l || r <= ql) return; if(ql <= l && r <= qr) {tag[i] += val, /*修改線段*/; return;} modify(ql, qr, val, l, m, i << 1), modify(ql, qr, val, m, r, i << 1 | 1); seg[i] = seg[i << 1] + seg[i << 1 | 1]; } ``` #### 查詢時 跟原本幾乎相同,只是在傳回值之前,也要把該線段的 `tag[i]` 清空。 ```cpp= LL query (int ql, int qr, int l = 0, int r = n, int i = 1){ /*修改線段*/ if(qr <= l || r <= ql) return 0LL; if(ql <= l && r <= qr) return seg[i]; return query(ql, qr, l, m, i << 1) + query(ql, qr, m, r, i << 1 | 1); } ``` 所以,**到底怎麼修改線段,又能把還沒修改的值留下來呢?** 其實就只要把 `tag[i]` 清空,修改 `seg[i]`,(修改值為 `(r - l) * tag[i]`) 然後把 `tag[i]` 的值傳到 `tag[i << 1]`、`tag[i << 1 | 1]` 就好了 這個函數會取名為 `push()` 因為很像把懶標記「推」到下方節點 ```cpp= void push(int l, int r, int i){ if(!tag[i]) return; if(r - l == 1) {seg[i] += tag[i], tag[i] = 0; return;} seg[i] += (r - l) * tag[i]; tag[i << 1] += tag[i], tag[i << 1 | 1] += tag[i], tag[i] = 0; } ``` #### 整合 ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; int maxn = 2e5 + 1, n; vector<LL> v(maxn), tag((maxn << 2) | 1), seg((maxn << 2) | 1); #define m (l + r) >> 1 void build(int l = 0, int r = n, int i = 1){ if(r - l == 1) {seg[i] = v[l]; return;} build(l, m, i << 1), build(m, r, i << 1 | 1); seg[i] = seg[i << 1] + seg[i << 1 | 1]; } void push(int l, int r, int i){ if(!tag[i]) return; if(r - l == 1) {seg[i] += tag[i], tag[i] = 0; return;} seg[i] += (r - l) * tag[i]; tag[i << 1] += tag[i], tag[i << 1 | 1] += tag[i], tag[i] = 0; } LL query (int ql, int qr, int l = 0, int r = n, int i = 1){ push(l, r, i); if(qr <= l || r <= ql) return 0LL; if(ql <= l && r <= qr) return seg[i]; return query(ql, qr, l, m, i << 1) + query(ql, qr, m, r, i << 1 | 1); } void modify(int ql, int qr, LL val, int l = 0, int r = n, int i = 1){ push(l, r, i); if(qr <= l || r <= ql) return; if(ql <= l && r <= qr) {tag[i] += val, push(l, r, i); return;} modify(ql, qr, val, l, m, i << 1), modify(ql, qr, val, m, r, i << 1 | 1); seg[i] = seg[i << 1] + seg[i << 1 | 1]; } #undef m int main(){ int T, op, l, r; LL val; cin >> n >> T; for(int i = 0; i < n; i++) cin >> v[i]; build(); while(T--){ cin >> op >> l >> r; if(op) cout << query(l - 1, r) << '\n'; else cin >> val, modify(l - 1, r, val); } } ``` --- ## 動態開點 有些題目很糟糕,需要陣列開 $10^8$ 的大小,顯然不行,記憶體會爆掉。 (陣列要**一次開完所有節點**) 但是這種題目都會有一個特點:**操作次數很少** 這時可以用指標,要用的節點再開,雖然**單一節點的記憶體肥**,但**整體來說還是贏陣列很多**的 $\Rightarrow$ 新增一個陣列值最多要開 $lg(maxn)$ 個節點, 如果導的出這個結論,那麼想法就應該對了。 ### 實作 #### 節點 node 就是線段樹上的每個線段,每個線段都有: 1. 本身的值 `v` 2. 自身的左、右子節點 `l, r` 整個 `node` 可以包裝成一個 `struct` 我們可以在宣告 `struct` 的同時也宣告線段樹的根節點 `root` 而在以下操作中,節點的初始化都是 `nullptr`,避免不必要的記憶體消耗 ```cpp= struct node { int v; node *l, *r; }*root = nullptr; ``` #### 建立線段樹 build() 由於我們是要用到節點再開,因此**沒有 `build()`** #### 遍歷 query() 和原本的線段樹雷同,不過: 1. 左節點的 `index` 從 `i << 1` 變成 `cur -> l` 2. 右節點的 `index` 從 `i << 1 | 1` 變成 `cur -> r` 3. 原先的傳回值從 `seg[i]` 變成 `cur -> v` 4. 由於是動態開點,可能存在 「目前節點不存在的問題」,判斷時要注意。 ```cpp= int query(int ql, int qr, node* cur = root, int l = 0, int r = n) { if (!cur || qr <= l || r <= ql) return 0; //!cur : 判斷是否有此節點 if (ql <= l && r <= qr) return cur -> v; return query(ql, qr, cur -> l, l, m) + query(ql, qr, cur -> r, m, r); } ``` #### 更新 update() (modify() 也是一樣的意思) 這裡講單點修改 也是跟原本陣列版的 update() 雷同,不過注意的地方和 query() 一樣 而且 update() 多了 「節點不存在就要創立」 這件事 因為 query() 遇到沒有節點時就是代表該節點是 $0$,根本不用算 但 update() 遇到沒節點...就是要更新線段樹 因此: ```cpp= void update(int p, int x, node* &cur = root, int l = 0, int r = n) { if (p < l || r <= p) return; if (!cur) cur = new node {0, nullptr, nullptr}; //建立新節點 cur -> v += x; if (r - l == 1) return; update(p, x, cur -> l, l, m), update(p, x, cur -> r, m, r); } ``` 所以說 `node* &cur` 是甚麼神操作? :::spoiler `answer` `node*` 是指標的型態,`&` 是參照,加了才能更改節點內的值 ::: #### 整合 ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; constexpr int n = 1e8 + 5; struct node { int v; node *l, *r; }*root = nullptr; #define m ((l + r) >> 1) void update(int p, int x, node* &cur = root, int l = 0, int r = n) { if (p < l || r <= p) return; if (!cur) cur = new node {0, nullptr, nullptr}; cur -> v += x; if (r - l == 1) return; update(p, x, cur -> l, l, m), update(p, x, cur -> r, m, r); } int query(int ql, int qr, node* cur = root, int l = 0, int r = n) { if (!cur || qr <= l || r <= ql) return 0; if (ql <= l && r <= qr) return cur -> v; return query(ql, qr, cur -> l, l, m) + query(ql, qr, cur -> r, m, r); } #undef m int main() { int T, op, key, val, l, r; cin >> T; while (T--) { cin >> op; if (op) cin >> key >> val, update(key, val); else cin >> l >> r, cout << query(l, r + 1) << '\n'; //這裡是 [l, r] } return 0; } ``` --- ## 小結 題目不搞怪,用線段樹模板 題目要你開超大陣列,用動態開點 題目要你區間修改,用懶標記 題目要區間修改又要開超大陣列,先想想有沒有更好的算法,沒有就...懶標記與動態開點整合... 或許比賽中放棄也是不錯的選擇 (怕你解這題的時間成本太高) ## 題目練習:DDJ a408 & a522 1. 題目連結:[a408: 區間更新](http://203.64.191.163/ShowProblem?problemid=a408) - 兩個操作:對一區間的所有值 $+k$,或是詢問 $[l, r]$ 區間和。 因為跟懶標記的範例程式碼太像,所以這邊就不貼了。 2. 題目連結:[a522: Mining for Gold (hard version)](http://203.64.191.163/ShowProblem?problemid=a522) - 兩個操作:單點值 $+1$ 或詢問 $[l, r]$ 區間和,不過 $[l, r]$ 範圍有夠大 不過對動態開點來說,這只不過是個模板題 :poop: :::spoiler `code` ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; constexpr int n = 1e8 + 5; struct node { int v; node *l, *r; }*root = nullptr; #define m ((l + r) >> 1) void update(int p, int x, node* &cur = root, int l = 0, int r = n) { if (p < l || r <= p) return; if (!cur) cur = new node {0, nullptr, nullptr}; cur -> v += x; if (r - l == 1) return; update(p, x, cur -> l, l, m), update(p, x, cur -> r, m, r); } int query(int ql, int qr, node* cur = root, int l = 0, int r = n) { if (!cur || qr <= l || r <= ql) return 0; if (ql <= l && r <= qr) return cur -> v; return query(ql, qr, cur -> l, l, m) + query(ql, qr, cur -> r, m, r); } #undef m int main() { cin.tie(nullptr), ios_base::sync_with_stdio(false); int T, op, k, a, b; cin >> T; while (T--) { cin >> op; if (op == 1) cin >> k, update(k, 1); else if (op == 2) { cin >> a >> b; cout << query(a, b + 1) / 6 * 48 << '\n'; } } return 0; } ``` :::