<style> .reveal .slides { text-align: left; font-size:35px; } </style> ## 線段樹 - 2 Introduction to Competitive Programming 4/3 ---- - 懶人標記 - 基礎區間修改應用 - 更多變化應用 --- ### 區間更新區間詢問 給一長度為 n 的整數序列,和 m 筆操作 操作分兩種 1. $\text{update l r v}$,將序列的區間 [l, r] 加值 v 2. $\text{query l r}$,詢問序列的區間 [l, r] 總和 $n,m<10^5$ ---- ### 使用單點修改線段樹 單點修改 [l , r] 範圍 時間複雜度最差的情況下,如果每次都加值整個序列, 為 $O(n\log n)$ 修改 m 次,時間複雜度 $O(mn\log n)$, 比直接修改原陣列 $O(mn)$ 還差! ---- ### 什麼是懶人標記 lazy tag 懶人標記的思想就是,如果所在的區間被修改的區間完全覆蓋的話,就在當前的區間打一個標記,代表這個區間以及下面所有區間都需要修改。 ---- ![image](https://hackmd.io/_uploads/SJHCarr21g.png=700x) 如果加值[2,6] 可以發現 [2,3] [4,5] [6,6] 區間都要打標記 ---- 為了維護好每個節點的Info,若是走到有打 lazy tag 的節點,必須把這個 tag 往下傳遞一層 ---- $\texttt{info}$ $\to$ 當前節點的總和 $\texttt{len}$ $\to$ 當前節點的長度 $\texttt{tag}$ $\to$ 當前節點的標記 $L_{\text{tag}}$ $\to$ 左子節點的標記 $R_{\text{tag}}$ $\to$ 右子節點的標記 如果是區間總和的線段樹 $\text{tag}$ 代表區間要被加值多少 $\texttt{info += tag * len}$ $L_{\text{tag}} = R_{\text{tag}} = \text{tag}$ $\text{tag} = 0$ $\text{tag}$設為0代表不用加值 ---- ### Push 函數 一個把tag往下傳遞的函數 維護總和的Push 函數 ```cpp! void push(int p, int l, int r) { if (tag[p] != 0) { info[p] += tag[p] * (r - l); if (r - l != 1) { // 如果是葉節點 -> 沒有左右子節點 (沒判會RE) tag[cl(p)] = tag[cr(p)] = tag[p]; } tag[p] = 0; } } ``` ---- ### Pull 函數 一個往上合併兩個節點資訊的函數 因為合併需要用到兩個子節點 所以要先把兩個子節點的tag往下傳遞 ```cpp= void pull(i32 p, i32 l, i32 r) { i32 m = (l + r) >> 1; push(cl(p), l, m); push(cr(p), m, r); info[p] = info[cl(p)] + info[cr(p)]; } ``` ---- ### 更新範例程式碼 ```cpp= void rangeModify(i32 p, i32 l, i32 r, i32 x, i32 y, i64 v) { push(p, l, r); if (l >= x && r <= y) { tag[p] += v; return; } i32 m = (l + r) >> 1; if (x < m) rangeModify(cl(p), l, m, x, y, v); if (y > m) rangeModify(cr(p), m, r, x, y, v); pull(p, l, r); } ``` ---- ### 查詢 對於查詢其實操作是跟單點修改線段樹差不多的, 只是要記得把走過的節點的懶人標記往下傳遞。 ---- 跟單點修改線段樹相比 多一行 ```push``` 函數來更新節點並把懶人標記往下傳遞 ```cpp= i64 rangeQuery(i32 p, i32 l, i32 r, i32 x, i32 y) { push(p, l, r); if (l >= y || r <= x) return 0; if (l >= x && r <= y) return info[p]; i32 m = (l + r)>> 1; return rangeQuery(cl(p), l, m, x, y) + rangeQuery(cr(p), m, r, x, y); } ``` --- ## 例題時間 ---- - 當前節點維護的資訊可以從左右子節點得到 通常只要資訊有這個性質 就能用線段樹維護 ---- ### 例題:[CSES: Polynomial Queries](https://cses.fi/problemset/task/1736/) 給一個長度為 n 的整數序列,和 m 筆操作 操作分兩種 * $\text{0 l r}$ : 將區間 [l, r] 範圍的元素 $a_l$ 加 1, $a_{l+1}$ 加 2 ... 以此列推 * $\text{1 l r}$ : 詢問區間 [l, r] 的總和 ---- 詢問為求區間總和, 因此只需要把等差加值好好的處理就做完了 ---- 可以發現,我們對區間的加值會是一個從 1 開始、公差為 1 的等差數列,我們便可以維護這個性質,因為只要有首項、項數、公差,就可以在 $O(1)$ 時間算出等差數列的總和(高中數學)。 ---- 項數就是那個區間的大小,而當一個等差數列疊加到另外一個等差數列上時 ```cpp 1 2 3 4 5 6 + 1 2 3 4 5 6 _______________________ 1 3 5 7 9 11 6 ‾ ‾ ‾ ‾ ‾‾ ``` 可以發現重疊的部分的首項跟公差就會變成兩者相加了! 中間重疊部分的公差直接就變成 2。 只需要維護首項還有公差,就可以算出總和了。 ---- ### 懶人標記下推 a是首項, d是公差 要注意的是右子區間的首項要小算一下。 ```cpp= void push(i32 p, i32 l, i32 r) { // need compelete if (tag[p].a != 0) { i32 len = r - l; i32 m = (r + l) >> 1; info[p].sum += (((2 * tag[p].a) + (len - 1) * tag[p].d) * len) / 2;; if (r - l != 1) { tag[cl(p)].a += tag[p].a; tag[cr(p)].a += tag[p].a + (m - l) * tag[p].d; tag[cl(p)].d += tag[p].d; tag[cr(p)].d += tag[p].d; } tag[p].a = 0; tag[p].d = 0; } } ``` ---- ### 區間更新 --- 打懶人標記 由於每次加的首項固定是 1,因此遞迴時不需要傳遞加值的參數 特別要注意的是,每個區間的首項要計算一下,不要直接設成1 ```cpp= void rangeModify(i32 p, i32 l, i32 r, i32 x, i32 y) { push(p, l, r); if (l >= x && r <= y) { tag[p].d++; tag[p].a += (l - x + 1); return; } i32 m = (l + r) >> 1; if (x < m) rangeModify(cl(p), l, m, x, y, v); if (y > m) rangeModify(cr(p), m, r, x, y, v); pull(p, l, r); } ``` ---- ## [Range Updates and Sums](https://cses.fi/problemset/task/1735) 給一長度為 n 的整數序列,和 m 筆操作 操作分**三種** 1. $\text{update l r v}$,將序列的區間 [l, r] 加值 v 2. $\text{update l r v}$,將序列的區間 [l, r] 都設值成 v 3. $\text{query l r}$,詢問序列的區間 [l, r] 總和 $n,m<10^5$ 多了一個區間設值 ---- 兩種操作單獨出現都可以解掉,但同時出現時要好好處理。 由於需要維護兩種懶標,我們還需要考慮同時有兩個懶標存在的情況。 ---- 兩個懶人標記 setTag 和 addTag 分別維護區間設值與區間加值 在維護標記時,可以分為 3 種情況 1. 本來沒有任何標記,現在再加值/設值 2. 本來有加值/設值標記,現在再加值 3. 本來有加值/設值標記,現在再設值 ---- ### 1. 本來沒有任何標記,現在再加值/設值 那就跟一般加值/設值線段樹一樣 打上加值或設值標記就好 ---- ### 2. 本來有加值/設值標記,現在再加值 - 本來有加值標記,那再加值有疊加性,不影響 - 本來有設值標記再加值,可以先設值完之後再加值 ---- ### 3. 本來有加值/設值標記,現在再設值 區間設值則之前所有的標記都要不用理他了 直接把原本的加值/設值都清空,設上新的值 ---- 綜合以上的 case,只需要好好處理以下情況 1. 更新區間時,同時有設值/加值標記時,先設值再加值 2. 有設值操作時,到覆蓋的區間先把原有的設值/加值標記先清空,再放上設值標記 其他跟一般線段樹相同 ! ---- BIT 的 code 比 segment tree 短很多, 遇到資料結構題時,請先想一下 BIT 是否可做(區間修改/單點詢問 or 單點詢問/區間修改) 如果可以的話請不要沒事亂砸線段樹 實作量較大,bug 可能也比較多 ---- [題單](https://vjudge.net/contest/706717)
{"description":"Introduction to Competitive Programming2/17","title":"Segment Tree 2","contributors":"[{\"id\":\"3a4ab387-f23d-466f-a3db-98c85d8d5efb\",\"add\":8543,\"del\":3759}]"}
    194 views
   Owned this note