# Segment Tree
「線段樹」酷酷的資料結構
## basic
現在的高中生人手一棵線段樹 BY NPSC裁判
一棵最簡單的線段樹支援以下幾種操作(對陣列)
* 單點改值
* 區間求值(區間和、區間最大最小)
**示意圖**
![](https://i.imgur.com/v8aCRFW.png)
觀察一下這張圖,一個節點需要記錄什麼資訊?
* 左界、右界
* 左節點、右節點 (存指標)
* 值 (圖中沒畫,儲存當前區間的值)
**Why 左閉右開?**
有人會覺得左閉右閉比較好,但根本邪教
* 一個區間的右界就會是下一個接續區間的左界(不用+1-1)
* $size = r-l$ (一樣不用+1-1)
所以開一個struct用來記錄每個節點:
```cpp=
struct seg {
int l, r, mid, val;
seg* ch[2] = {}; //two children
};
```
**How to construct a segment tree?**
* 如果 $l+1 \not= r$, 繼續往下開點
* 如果 $l+1 = r$, 結束
code:
```cpp=
struct seg{
int l, r, mid, val;
seg *ch[2] = {};
seg(int _l, int _r):l(_l),r(_r){
mid = (l+r)/2;
if(l == r-1){
return;
}
ch[0] = new seg(l, mid); //left child
ch[1] = new seg(mid, r); //right child
}
};
```
蓋好線段樹了,如何單點改值?
\
**單點改值**
操作 : 將$a_{idx}$ 改為 $a_{idx} + k$ (也可以直接重設)
* 如果$idx<mid$ ,往左邊更新節點,否則$idx\geq mid$ ,往右邊更新節點
* 如果$l = r-1$ ,代表到目標了,更新節點
* 回來時要更新上面的節點(pull)
**pull**
當一個節點的的下方有更改過,就要將資訊**拉**(更新)上來,就是pull
code:
```cpp=
void pull(){
val = ch[0]->val + ch[1]->val;
}
void add(int idx, int k){
if(l == r-1){
val += k;
return;
}
if(idx < mid){
ch[0]->add(idx, k);
}
else{
ch[1]->add(idx, k);
}
pull();
}
```
特別注意指標呼叫成員用->
**區間求值**
操作:給一區間$[a, b)$ ,求區間和
* 如果$a < mid$ ,代表所求區間有跨到左邊,向左邊找答案
* 如果$b > mid$ ,代表所求區間有跨到右邊,向右邊找答案
* 如果$[a, b)$完全包含$[l, r)$,回傳值
* 統整當前所有答案並往上回傳(求區間和,所以要相加)
code:
```cpp=
int query(int a, int b){
if(a <= l && r <= b){
return val;
}
int ans = 0;
if(a < mid){
ans += ch[0]->query(a, b);
}
if(b > mid){
ans += ch[1]->query(a, b);
}
return ans;
}
```
大功告成!!
### 例題
[**Static Range Minimum Queries**](https://cses.fi/problemset/task/1647)
[**Dynamic Range Sum Queries**](https://cses.fi/problemset/task/1648)
[**Dynamic Range Minimum Queries**](https://cses.fi/problemset/task/1649)
[**Range Xor Queries**](https://cses.fi/problemset/task/1650)
**Tip**
善用pull與參照,直接在建構線段樹時設好初始值,不用n次單點改值
code:
```cpp=
struct seg{
int l, r, mid, val;
seg *ch[2] = {};
seg(int _l, int _r, vector<int> &arr):l(_l),r(_r){
mid = (l+r)/2;
if(l == r-1){
val = arr[l];
return;
}
ch[0] = new seg(l, mid, arr);
ch[1] = new seg(mid, r, arr);
pull();
}
void pull(){
val = ch[0]->val + ch[1]->val;
}
};
```
## Lazy Tag
懶人標記,俗稱懶標,用來記錄當前區間**已經**做過的操作(子節點尚未更新),等到需要**往下修改或查詢**的時候再將資訊推下去。
**區間加值**
操作:將區間$[a,b)$內的每一項加$k$
跟query(區間查詢)作法很像,當$[a, b)$完全包含$[l, r)$ :
* 將 $lz$ 改為 $lz+k$ (標記該區間每一項都$+k$ )
* 將 $val$ 改為 $val+ k \times(r-1)$
* 做完要$pull$
這時候query(區間和)往下的時候就要記得$push$
**Push ??**
push 怎麼做?
* 將左右子樹的$val$及$lz$按照當前區間的$lz$修改,並重設$lz$
code:
```cpp=
struct seg{
int l, r, mid, val, lz = 0;
seg *ch[2] = {};
seg(int _l, int _r, vector<int> &arr):l(_l),r(_r){
mid = (l+r)/2;
if(l == r-1){
val = arr[l];
return;
}
ch[0] = new seg(l, mid, arr);
ch[1] = new seg(mid, r, arr);
pull();
}
void pull(){
val = ch[0]->val + ch[1]->val;
}
void push(){
if(!lz) return;
ch[0]->lz += lz;
ch[1]->lz += lz;
ch[0]->val += lz*(mid - l);
ch[1]->val += lz*(r - mid);
lz = 0;
}
void add(int a, int b, int k){
if(a <= l && r <= b){
val += k*(r-l);
lz += k;
return;
}
if(a < mid){
ch[0]->add(a, b, k);
}
if(b > mid){
ch[1]->add(a, b, k);
}
pull();
}
int query(int a, int b){
if(a <= l && r <= b){
return val;
}
push();
int ans = 0;
if(a < mid){
ans += ch[0]->query(a, b);
}
if(b > mid){
ans += ch[1]->query(a, b);
}
return ans;
}
};
```
以上是懶標的基礎運用,懶標的概念就是紀錄做了什麼事,等到要查詢再$push$就好
### 例題
[**Range Update Queries**](https://cses.fi/problemset/task/1651)
[**Subarray Sum Queries**](https://cses.fi/problemset/result/4200393/)
寫完下面這題代表你會用懶標了
[**Range Updates and Sums**](https://cses.fi/problemset/task/1735)
## Persistent Segment Tree
持久化線段樹,神奇的資料結構,可以在修改後還能查詢先前的版本。
示意圖
![](https://i.imgur.com/4HSqNu4.gif)
藍色為第一版本,其他為後續。
如何開一顆新節點? 需有人對照(舊版本)
* $l, r, mid, val, lz$都照抄
* 往下改點($replace, add$)或是更新懶標($push$)的時候,將左節點設為參考原左節點而開成的新點,右邊亦然
* 查詢時直接查,$push$記得開點
code:
```cpp=
seg(seg *root){
l = root->l;
r = root->r;
mid = (l+r)/2;
if(l == r-1) return;
ch[0] = root->ch[0];
ch[1] = root->ch[1];
pull();
}
void pull(){
val = ch[0]->val + ch[1]->val;
}
void replace(int idx, int k){
if(l == r-1){
val = k;
return;
}
if(idx < mid){
ch[0] = new seg(ch[0]);
ch[0]->replace(idx, k);
}
else{
ch[1] = new seg(ch[1]);
ch[1]->replace(idx, k);
}
pull();
}
```
constructor,query 一樣,故省略(此為無懶標版本)
補充:如果不想打1~9行的constructor可以這樣寫:
```cpp=
ch[0] = new seg(*ch[0]);
```
new 代表會新開一個空間,而新的seg的成員變數都會跟原本一樣
### 例題(裸題,直接套模板就行)
[**Range Queries and Copies**](https://cses.fi/problemset/task/1737)
### 區間K-th問題
給一陣列$A$ 每次詢問$[l, r]$間第k大的是誰
想法:
如果每次詢問都滿足$l = 0$,那我們可以對值域開一棵線段樹,存每個區間的數字出現的次數,再來只要在線段樹上二分搜就行(找第一個前綴和會大於等於k的地方)。
$l\neq0$ ?
接下來只要開一棵持久化線段樹,第$i$個版本代表紀錄前$i$項的線段樹,在查詢$[l, r]$的時候用$r$跟$(l-1)$兩個版本相減就好
例題:
[**Range Kth Smallest**](https://judge.yosupo.jp/problem/range_kth_smallest)
:::spoiler Code
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define all(x) x.begin(),x.end()
struct seg{
int l, r, mid, val;
seg *lc, *rc;
seg(){};
seg(int _l, int _r):l(_l),r(_r),mid((l+r)>>1){
if(l == r-1){
val = 0;
return;
}
lc = new seg(l, mid);
rc = new seg(mid, r);
pull();
}
void pull(){
val = lc->val + rc->val;
}
void add(int idx, int k){
if(l == r-1){
val += k;
return;
}
if(idx < mid){
lc = new seg(*lc);
lc->add(idx, k);
}
else{
rc = new seg(*rc);
rc->add(idx, k);
}
pull();
}
};
int query(seg *a, seg *b, int k){ //find (k+1)-th
if(a->l == a->r-1) return a->l;
if(b->lc->val-a->lc->val > k) return query(a->lc, b->lc, k);
else return query(a->rc, b->rc, k-(b->lc->val-a->lc->val));
}
signed main(){
int n, q;
cin >> n >> q;
vector<int> arr(n), lisan(n);
for(int i = 0; i < n; i++) cin >> arr[i];
lisan = arr;
sort(all(lisan));
lisan.resize(unique(all(lisan))-lisan.begin());
vector<seg*> sg(n+1);
sg[0] = new seg(0, lisan.size());
for(int i = 0; i < n; i++){
sg[i+1] = new seg(*sg[i]);
sg[i+1]->add(lower_bound(all(lisan), arr[i])-lisan.begin(), 1);
}
for(int i = 0,a,b,k; i < q; i++){
cin >> a >> b >> k;
cout << lisan[query(sg[a], sg[b], k)] << endl;
}
}
```
:::
## Li-chao tree
李超線段樹,一棵神奇的線段樹,維護一個集合(存線),給一個$x$,求最小的$f(x)$(或最大)
目標:
* 開一個線段樹在$x$軸,每個節點存一條線$(val)$
* 保證$query$到$x_1$所經過的節點上的線$f_1(x),f_2(x) \cdots f_n(x)$,均代入$x_1$取$min$即為答案
假設一節點$[l,r)$存著一線$a$(紅色),插入一線$b$(藍色),則有四種情況:
1. $f_a(mid)<f_b(mid)\wedge m_a > m_b$
![](https://i.imgur.com/TMQoflh.png)
原節點維護之線不變,將$b$插入右子節點更新(可能更優)
2. $f_a(mid)<f_b(mid)\wedge m_a < m_b$
![](https://i.imgur.com/5kyrnyi.png)
原節點維護之線不變,將$b$插入左子節點更新(可能更優)
3. $f_a(mid)>f_b(mid)\wedge m_a > m_b$
![](https://i.imgur.com/YDqkNV8.png)
原節點維護之線改為$b$,將$a$插入左子節點更新(可能更優)
4. $f_a(mid)>f_b(mid)\wedge m_a < m_b$
![](https://i.imgur.com/BFX6CRZ.png)
原節點維護之線改為$b$,將$a$插入右子節點更新(可能更優)
**總結**
* 將$f(mid)$較小的線設為當前節點的$val$
* 另一條線若斜率較大,往左邊更新,反之則往右邊
* 若$f(mid)$較大的線完全在另一線上方,直接$return$
* $query$時,經過的所有線$(val)$取$mid$即為答案
code:
```cpp=
struct line{
int a, b;
line(int _a, int _b):a(_a),b(_b){}
int f(int x){
return a*x + b;
}
};
struct seg{
int l, r, mid;
line val = line(0, 1e18);
seg *ch[2] = {};
seg(int _l, int _r):l(_l),r(_r){
mid = (l+r)/2;
if(l == r-1) return;
ch[0] = new seg(l, mid);
ch[1] = new seg(mid, r);
}
void insert(line k){
if(k.f(mid) < val.f(mid)) swap(k, val);
if(l == r-1) return;
if(k.a > val.a){
ch[0]->insert(k);
}
else{
ch[1]->insert(k);
}
}
int query(int x){
if(l == r-1){
return val.f(x);
}
if(x < mid){
return min(val.f(x), ch[0]->query(x));
}
else{
return min(val.f(x), ch[1]->query(x));
}
}
};
```
### 例題
[**Shopping in AtCoder store**](https://atcoder.jp/contests/abc289/tasks/abc289_g)
:::spoiler 解法
看完題目後,可以發現當我們考慮調整第$j$物品的價格時,
先將$B$由大到小排序
如果想要讓$i$個人購買物品,那最大的$P_j = B_i+C_j$
總獲利為$i\times(B_i+C_j)$ 整理後得 $iC_j+iB_i$
但枚舉$i=[1,n]$會TLE,所以我們考慮可以插入$n$條直線$f_i(x) = ix+iB_i$
接下來只要由給定的$C_j$,找到$\displaystyle\max_{\forall i\in [1,n]}iC_j+iB_i$ 就行了
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define all(x) x.begin(),x.end()
struct line{
int a, b;
line(int _a, int _b):a(_a),b(_b){}
int f(int x) {
return a*x+b;
}
};
struct seg{
int l, r, mid;
line val = line(0, 0);
seg *lc = NULL, *rc = NULL;
seg(int _l, int _r):l(_l),r(_r),mid((l+r)>>1){}
void newNode(){
if(lc != nullptr || l == r-1)return;
lc = new seg(l, mid);
rc = new seg(mid, r);
}
void insert(line k){
if(k.f(mid) > val.f(mid)) swap(k, val);
if(l == r-1)return;
newNode();
if(k.a > val.a) rc->insert(k);
else lc->insert(k);
}
int query(int idx){
if(l == r-1 || lc == nullptr) return val.f(idx);
int ans = val.f(idx);
if(idx < mid) ans = max(ans, lc->query(idx));
else ans = max(ans, rc->query(idx));
return ans;
}
};
signed main(){
int n, m;
cin >> n >> m;
vector<int> b(n), c(m);
seg sg(0, 1e9);
for(int i = 0; i < n; i++) cin >> b[i];
for(int i = 0; i < m; i++) cin >> c[i];
sort(all(b));
reverse(all(b));
for(int i = 0; i < n; i++) sg.insert(line(i+1,(i+1)*b[i]));
for(int i = 0; i < m; i++) cout << sg.query(c[i]) << " ";
cout << endl;
}
```
:::
\
\
[**Monster Game II**](https://cses.fi/problemset/task/2085/)
用李超線段樹把DP過程優化吧
:::spoiler 解法
考慮$dp[i]$為打倒第$i$隻怪獸所花費的最小總時間,且打完第$j$之後打$i$所花費的時間為$s[i]\times f[j]$,所以
$dp[i] = \displaystyle{\min_{\forall j\in [0,i-1]}}s[i]\times f[j] + dp[j]$
砸李超線段樹就過了
:::
### 其他應用
[**Segment Add Get Min**](https://judge.yosupo.jp/problem/segment_add_get_min)
插入直線變成插入線段怎麼辦? 其實原本插入的就是在該區間範圍的直線,所以只需要將要插的直線範圍拆成$\log n$個區間插入就好:
```cpp=
void insert(int a, int b,line k){
if(a <= l && r <= b) return insert_line(k);
if(a < mid) lc->insert(a, b, k);
if(b > mid) rc->insert(a, b, k);
}
void insert_line(line k){
if(k.f(mid) < ln.f(mid)) swap(k, ln);
if(l == r-1){
val = min(ln.f(l),ln.f(r-1));
return;
}
if(k.a > ln.a) lc->insert_line(k);
else rc->insert_line(k);
}
```
當然也是可以把兩個函式合併。
#### 誰說一定要直線??
![](https://i.imgur.com/choBigd.png)
李超線段樹適用於具有優超性的函數 只要函數$f_1$在某個地方被$f_2$超越後,$f_1$就不可能再反超。
## Li-chao tree extended
在 2021 年 1 月,有人在 Codeforces 上發表了擴充版的李超線段樹。
可以處理以下兩種問題
**問題一**
有一個大小為$N$的陣列$A$,還有$Q$筆操作:
* 區間插入線,給定$l,r,a,b$,做 $A_i = \max (A_i,a \cdot i + b)$, $\forall i \in [l, r]$ in $O(\log^{2} N)$
* 區間加上線,給定$l,r,a,b$,做 $A_i += a \cdot i + b$, $\forall i \in [l, r]$ in $O(\log^{2} N)$
* 單點查詢值,給定$i$,回傳$A_i$ in $O(\log N)$
**問題二**
有一個大小為$N$的陣列$A$,還有$Q$筆操作:
* 區間插入線,給定$l,r,a,b$,做 $A_i = \max (A_i,a \cdot i + b)$, $\forall i \in [l, r]$ in $O(\log^{2} N)$
* 區間加單值,給定$l,r,b$,做 $A_i += b$, $\forall i \in [l, r]$ in $O(\log^{2} N)$
* 區間取極值,給定$l, r$,回傳$\displaystyle\max_{\forall i\in [l, r] } A_i$ in $O(\log N)$
\
\
一般李超線段樹是從可能成為答案的直線取$max$,插入順序是具有交換律的。
而有了區間加線後,插入的線是在加線前加入還是在加線後插入就差很多了。
\
以原版的李超來舉例:
* $[1, N]$ 一開始為空
* 在$[1, N]$插入$y = 1$ ,然後在$[2, N]$ 加上 $y = -\infty$
* 很顯然$[1,N]$ 不包含在 $[2,N]$ 所以$[1, N]$這個節點不受到加值影響
* 查詢$x = 2$ 時,會從 $[1,N]$這個節點查到 $y = 1$這條線,而得到錯誤的結果$1$而不是$-\infty$
\
考慮這種情況,答案其實很明顯:
加線段的時候在經過 $[l, r]$這個節點時,就先把它上面的直線$f$清掉,並對$[l, mid]$和$[mid, r]$插入$f$。
這個情況下,加入的線段就不能直接推到葉子,要用懶標存在節點上,才可以保證複雜度依然是 $O(N \log^2 N)$。
\
\
問題二其實也差不多
但區間加直線會造成區間最大值的位置改變,若最大值發生在底下的直線,會造成懶人標記的不可預測性,因此作者才在第二種維護中將該操作降級成了單純的區間加值。
**code 1**
```cpp=
struct line{
int a = 0, b = 0;
line(){}
line(int _a, int _b):a(_a),b(_b){}
int f(int x){
return b+a*x;
}
bool operator ==(const line &next)const{
return (a == next.a && b == next.b);
}
void operator +=(const line &next){
a += next.a;
b += next.b;
}
};
struct seg{
int l, r, mid;
line ln = line(0, 0), lazy = line(0, 0);
seg *lc = NULL, *rc = NULL;
seg(int _l, int _r):l(_l),r(_r),mid((l+r)>>1){}
void newNode(){
if(l == r-1)return;
lc = new seg(l, mid);
rc = new seg(mid, r);
}
void push_lazy(){
if(lazy == line(0, 0))return;
lc->lazy += lazy;
rc->lazy += lazy;
lc->ln += lazy;
rc->ln += lazy;
lazy = line(0, 0);
}
void push_line(){
lc->insert_line(ln);
rc->insert_line(ln);
ln = line(0, 0);
}
void insert_line(line k){
if(k.f(mid) < ln.f(mid)) swap(k, ln);
if(l == r-1)return;
if(lc == nullptr) newNode();
push_lazy();
if(k.a > ln.a) lc->insert_line(k);
else rc->insert_line(k);
}
void insert(int a, int b,line k){
if(a <= l && r <= b) return insert_line(k);
if(lc == nullptr) newNode();
push_lazy();
if(a < mid) lc->insert(a, b, k);
if(b > mid) rc->insert(a, b, k);
}
void add(int a, int b, line k){
if(a <= l && r <= b){
lazy += k;
ln += k;
return;
}
if(lc == nullptr) newNode();
push_lazy();
push_line();
if(a < mid) lc->add(a, b, k);
if(b > mid) rc->add(a, b, k);
}
int query(int idx){
if(l == r-1 || lc == nullptr) return ln.f(idx);
push_lazy();
int ans = ln.f(idx);
if(idx < mid) ans = max(ans, lc->query(idx));
else ans = max(ans, rc->query(idx));
return ans;
}
};
```
**code 2**
```cpp=
struct seg{
int l, r, mid;
int val = 0,lazy = 0;
seg *lc = NULL, *rc = NULL;
line ln = line(0, 0);
seg(int _l, int _r):l(_l),r(_r),mid((l+r)>>1){}
void newNode(){
if(l == r-1)return;
lc = new seg(l, mid);
rc = new seg(mid, r);
}
void push_lazy(){
if(lazy == 0)return;
lc->ln.b += lazy;
rc->ln.b += lazy;
lc->lazy += lazy;
rc->lazy += lazy;
lc->val += lazy;
rc->val += lazy;
lazy = 0;
}
void push_line(){
lc->insert_line(ln);
rc->insert_line(ln);
ln = line(0, INF);
}
void pull(){
val = max(max(lc->val,rc->val),max(ln.f(l),ln.f(r-1)));
}
void insert_line(line k){
if(k.f(mid) < ln.f(mid)) swap(k, ln);
if(l == r-1){
val = max(ln.f(l),ln.f(r-1));
return;
}
if(lc == nullptr) newNode();
push_lazy();
if(k.a > ln.a) lc->insert_line(k);
else rc->insert_line(k);
pull();
}
void insert(int a, int b,line k){
if(a <= l && r <= b) return insert_line(k);
if(lc == nullptr) newNode();
push_lazy();
if(a < mid) lc->insert(a, b, k);
if(b > mid) rc->insert(a, b, k);
pull();
}
void add(int a, int b, int k){
if(a <= l && r <= b){
lazy += k;
ln.b += k;
val += k;
return;
}
if(lc == nullptr) newNode();
push_lazy();
push_line();
if(a < mid) lc->add(a, b, k);
if(b > mid) rc->add(a, b, k);
pull();
}
int query(int a, int b){
if(a <= l && r <= b) return val;
if(lc == nullptr) return max(ln.f(a), ln.f(b-1));
push_lazy();
int ans = max(ln.f(a), ln.f(b-1));
if(a < mid) ans = max(ans, lc->query(a, b));
if(b > mid) ans = max(ans, rc->query(a, b));
return ans;
}
};
```
因為RMQ要預處理區間$\max$所以有些細節要注意,有點麻煩。
### 例題
[**I hate Shortest Path Problem**](https://atcoder.jp/contests/abc177/tasks/abc177_f)
\
\
\
\
參考資料:
[**[Tutorial] Li Chao Tree Extended**](https://codeforces.com/blog/entry/86731)
ioic2023 講義