# 進階資料結構與莫隊算法—線段樹 ###### tags: `108 師大附中校隊培訓` ###### wiwiho --- ## 什麼是線段樹 ---- 把塊分割 ---- 整個序列列是一大塊 這一塊可以二等分成兩小塊 每一小塊又可以再二等分 這樣一直二等分下去 到最後會分到每一塊都剩下一個元素 ---- 這樣可以畫成一棵二元樹 ---- 根節點表示整個序列 它的左子節點表示它的左半部 右子節點表示它的右半部 ---- 每個節點的左子節點 都表示該節點所表示範圍的左半部 右子節點表示右半部 葉節點則表示只有一個元素的區間 ---- 每一個節點都表示一個區間 而每個節點表示的區間都 完整包含了其子節點所表示的區間 ---- ![](https://i.imgur.com/sC6SBjn.png =600x) ---- ## 索引要從 $0$ 或從 $1$ 開始? 實作上幾乎沒差,看習慣 ---- ## 區間表示要左閉右閉($[l,r]$)或左閉右開($[l,r)$)? 實作上差不多,也看習慣 ---- 在每一個節點上 儲存在那個區間中我們想要的答案 父節點的答案可以從兩個子節點來得到 --- ## 儲存 ---- 二元樹有兩種存法 1. 指標 2. 陣列 ---- 不動態開點的話 線段樹通常都用陣列 ---- 線段樹的每個節點的兩子樹會是平衡的 因此除了它最深的那一層 其他層都會是滿的 ---- - 根節點的編號是 $0$ - $i$ 的左子節點編號會是 $2i+1$ - 右子節點編號是 $2i+2$ - 根節點編號是 $1$ - 節點 $i$ 的左子節點編號是 $2i$ - 右子節點是 $2i+1$ 用哪種?看習慣就好 ---- ## 陣列該開多大? ---- 看節點編號最多會用到多少 ---- 有 $2^i$($i$ 是非負整數)個葉節點的滿二元樹 - 非葉節點會有 $2^i-1$ 個 - 總節點數是 $2^{i+1}-1$ ---- 線段樹的葉節點數和序列長度是一樣的 如果這個數字是 $n$,那麼葉節點會有 $n$ 個 ---- 如果把最後一層填滿 會有 $2^{\lceil\log_2n\rceil}$ 個葉節點 整棵樹會有 $2^{\lceil\log_2n\rceil+1}-1$ 個節點 ---- 如果 $n=2^i+a$,$0<a<2^i$ $2^{\lceil\log_2n\rceil}=2^{i+1}$ $2^{\lceil\log_2n\rceil+1}-1=2^{i+2}-1<2^{i+2}$ ---- $n \times 4$ 一定會大於總節點數 陣列大小通常都會取 $n \times 4$ --- ## 建構 ---- 一直遞迴去把區間二等分填數字就好 ---- ``` array segment_tree; //用來存線段樹的那個陣列,用普通陣列或vector都可 array a; //序列 int n; //a的大小 //v的區間是[L,R] void build(int L, int R, vertex v){ if(L == R){ a[v] = 用a[i]得出答案; return; } int M = (L + R) / 2; //把區間二等分 build(L, M, v的左子節點); build(M + 1, R, v的右子節點); } ``` ---- 複雜度和節點數一樣 是 $O(n)$ --- ## 單點修改 ---- 先找到表示那個點的葉節點 並且更新它和它的所有祖先 ---- ``` //節點 v 的區間範圍是[L,R],要修改的地方是pos,新值是value void modify(type value, int pos, int L, int R, vertex v){ if(L == R){ //找到了葉節點 修改 v; return; } int M = (L + R) / 2; //[L,R]區間的中點 //將這個區間二等分,左半的範圍是[L,R],右半是[M+1,R] if(pos >= L && pos <= M) //pos在左半部 modify(value, pos, L, M, v的左子節點); else //pos在右半部 modify(value, pos, M+1, R, v的右子節點); 根據v的左子節點和右子節點的答案更改v的答案; } ``` ---- 線段樹的深度最深就是 $\lceil\log_2n\rceil$ 因此遞迴次數為 $O(\log n)$ 時間複雜度 $O(\log n)$ --- ## 區間查詢 ---- 遇到有一個節點的範圍 被完整包含在查詢範圍內 可以直接取這個節點記錄的答案 而不用往下找 ---- ``` //查詢範圍是[l,r],節點v的區間範圍是[L,R] type query(int l, int r, int L, int R, vertex v){ if(l == L && r == R) //[L,R]完整被查詢區間所包含 return ans of v; int M = (L + R) / 2; if(r <= M) //如果查詢區間都在左半部,就只查左半部 return query(l, r, L, M, v的左子節點); else if(l > M) //如果查詢區間都在右半部,就只查右半部 return query(l, r, M + 1, R, v的右子節點); else{ //如果查詢區間跨越兩半部,那就也把查詢區間切半 //這樣可以確保遞迴後[l,r]一定在[L,R]之內 return 用query(l, M, L, M, v的左子節點) 和query(M + 1, r, M + 1, R, v的右子節點) 得出區間[l,r]的答案; } } ``` ---- ![](https://i.imgur.com/INBqUEL.png =600x) ---- 被遞迴到的點一開始會是一條線 直到表示兩個端點的葉節點的 LCA 然後開始分叉成兩條畫到兩個端點的線 還有左分叉線上的每一個節點的右子節點 與右分叉線上的每一個節點的左子節點 ---- ![](https://i.imgur.com/TxI5OBT.png =200x) ---- 這數量最多就差不多深度的四倍而已 複雜度也是 $O(\log n)$ --- ## 懶人標記 ---- 區間修改? 用懶人標記 ---- 在節點上記錄對答案的變動 ---- 查詢時要往下遞迴 必須要把經過節點的懶人標記往下推 這樣在查子節點的時候答案才是對的 ---- ``` //新值是value,修改區間是[l,r],節點v的範圍是[L,R] void modify(type value, int l, int r, int L, int R, vertex v){ if(l == L && r == R){ 打懶標在v上; return; } int M = (L + R) / 2; if(r <= M) modify(value, l, r, L, M, v的左子節點); else if(l > M) modify(value, l, r, M + 1, R, v的右子節點); else{ modify(value, l, M, L, M, v的左子節點); modify(value, M + 1, r, M + 1, R, v的右子節點); } 用兩個子節點的答案更新v的答案; } ``` ---- ``` //查詢範圍是[l,r],節點v的區間範圍是[L,R] type query(int l, int r, int L, int R, vertex v){ if(l == L && r == R) return ans of v; 把節點v的懶標下推; //<---- int M = (L + R) / 2; if(r <= M) return query(l, r, L, M, v的左子節點); else if(l > M) return query(l, r, M + 1, R, v的右子節點); else{ return 用query(l, M, L, M, v的左子節點) 和query(M + 1, r, M + 1, R, v的右子節點) 得出區間[l,r]的答案; } } ``` ---- 複雜度和區間查詢一樣 $O(\log n)$ --- ## 動態開點 ---- 在動態開點的時候 用指標來存線段樹 ---- 區間範圍非常大 而且題目讓你也很難離散化 ---- 陣列開線段樹 記憶體一定不夠 但是根本只會用到其中幾個節點而已 ---- 可以用動態開點 只要開你需要的節點就好 --- ## 高維線段樹 ---- 線段樹可以有很多維度 在每個 $D$ 維線段樹的節點放一棵 $D-1$ 維的線段樹 ---- 在每一個節點都要往下查低一維的線段樹 所以複雜度是 $\log_2^Dn$
{"metaMigratedAt":"2023-06-15T01:16:40.563Z","metaMigratedFrom":"Content","title":"進階資料結構與莫隊算法—線段樹","breaks":false,"contributors":"[{\"id\":\"02353542-5acb-4a66-abd0-128b1af24abb\",\"add\":4299,\"del\":2}]"}
    1895 views