---
tags: 成大高階競技程式設計 2021
---
# 2021 Week8 - Range Query
註::bulb: 代表相對少見、較進階或與主題關聯性較低的內容,讀者可自行斟酌觀看。
---
之前我們透過前綴和(prefix sum)計算區間和,
可是需要修改陣列元素時,複雜度為 $O(n)$,
有沒有同時可以顧及修改與查詢效率的辦法呢?
又或者要問區間最小值(RMQ, Range Minimum Query)呢?
> 回到之前分治的想法,如果問一個區間 $[l:r)$,
> 我們把它對半拆成 $[l:m)$ 與 $[m:r)$ 有用嗎?
> 好像沒有,因為也不能比線性時間更快速地計算,
> 那如果是像前綴和一樣利用預先算好的值呢?
> 好像也不行,畢竟我們不能事先知道 $[l:r)$,
> 也就不能只算某些區間的值,讓不同的詢問都可以用。
我們照著整個陣列的大小對半分開成二個子陣列,
$[l:r)$ 中屬於左邊的與屬於右邊的就變成兩個子區間(子問題),
這樣的**切分方式與每次詢問的區間沒有關係**,
我們可以照著這個方式,左邊的左邊,左邊的右邊,
右邊的左邊,右邊的右邊,一直將子區間切分。
> 想想原本計算量比較大的,是較大的區間,再切分後會發生什麼事?
> 很多子區間剛好是整個子陣列,因此線段樹就是提前計算這些子陣列的和。
---
# 基本線段樹(Basic Segment Tree)
長度為 $6$ 的陣列建構而成的線段樹:
```graphviz
digraph {
n0 [shape=ellipse, label="[0:6)", fixedsize=true, width=1.0,height=0.5];
n1 [shape=ellipse, label="[0:3)", fixedsize=true, width=0.9,height=0.5];
n2 [shape=ellipse, label="[3:6)", fixedsize=true, width=0.9,height=0.5];
n3 [shape=ellipse, label="[0]", fixedsize=true, width=0.5,height=0.5];
n4 [shape=ellipse, label="[1:3)", fixedsize=true, width=0.8,height=0.5];
n5 [shape=ellipse, label="[3]", fixedsize=true, width=0.5,height=0.5];
n6 [shape=ellipse, label="[4:6)", fixedsize=true, width=0.8,height=0.5];
n9 [shape=ellipse, label="[1]", fixedsize=true, width=0.5,height=0.5];
n10 [shape=ellipse, label="[2]", fixedsize=true, width=0.5,height=0.5];
n13 [shape=ellipse, label="[4]", fixedsize=true, width=0.5,height=0.5];
n14 [shape=ellipse, label="[5]", fixedsize=true, width=0.5,height=0.5];
n0 -> n1;
n0 -> n2;
n1 -> n3;
n1 -> n4;
n2 -> n5;
n2 -> n6;
//n3 -> n7;
//n3 -> n8;
n4 -> n9;
n4 -> n10;
//n5 -> n11;
//n5 -> n12;
n6 -> n13;
n6 -> n14;
}
```
> 定義 $[l,r)\ in\ [b,e)$,代表在子陣列 $[b:e)$ 中,查詢的子區間為 $[l,r)$。
>
> 假設要查詢 $[1:6)$,一開始問題是 $[1,6)\ in\ [0,6)$,
> 分成 $[1,3)\ in\ [0,3)$ 與 $[3,6)\ in\ [3,6)$ 二個子問題,
> $[1,3)\ in\ [0,3)$ 可以再切分,變成 $[1,3)\ in\ [1,3)$ ,
> 接著把 $[1,3)$ 和 $[3,6)$ 二個子陣列事先算好的值加起來就是答案了。
<hr class="dashed">
## 結構(Structure)
先來分析一下線段樹的結構。
:::danger
長度為 $n$ 的陣列建構而成的線段樹,需要 **$2n-1$** 個節點。
:::
> 令集合 $S$ 中有若干個節點,所有陣列元素都恰好包含在一個節點之中;
> 一開始 $S$ 中有 $n$ 個節點(樹葉),每個節點都包含一個陣列元素,
> 每次我們將 $S$ 中的二個節點取出,合併成一個新節點,再放入 $S$ 中,
> 經過 $n-1$ 次操作,$S$ 中就只剩下一個節點(樹根),包含所有陣列元素;
> 因此除了原本的 $n$ 個節點以外,還有 $n-1$ 個新節點,總共 $2n-1$。
:::danger
長度為 $n$ 的陣列建構而成的線段樹,樹高為 **$\lceil \log_2n\rceil$**。
:::
> 陣列長度為 $n'$,會分長度為 $\lfloor \frac {n'}{2}\rfloor$ 與 $\lceil \frac {n'}{2}\rceil$ 的二個子陣列,
> 如果一開始長度為 $n$,較長的一邊每次變為 $\lceil \frac {n'}{2}\rceil$,需要 $\lceil \log_2n\rceil$ 次才會變為 $1$。
<hr class="dashed">
## 儲存(Storage)
> 我們到底要怎麼儲存這個資料結構呢?
> 最簡單的方式就是透過指標(pointer)來儲存樹的邊,
> 但這好像不是個好辦法,除了實作比較麻煩一點,
> 整個樹狀結構也不會再改變,沒有必要用到指標。
線段樹很接近完美二元樹(perfect binary tree),那就把它補齊,就可以進行編號了。
```graphviz
digraph {
n0 [shape=ellipse, label="1", fixedsize=true, width=1.0,height=0.5];
n1 [shape=ellipse, label="2", fixedsize=true, width=0.9,height=0.5];
n2 [shape=ellipse, label="3", fixedsize=true, width=0.9,height=0.5];
n3 [shape=ellipse, label="4", fixedsize=true, width=0.5,height=0.5];
n4 [shape=ellipse, label="5", fixedsize=true, width=0.8,height=0.5];
n5 [shape=ellipse, label="6", fixedsize=true, width=0.5,height=0.5];
n6 [shape=ellipse, label="7", fixedsize=true, width=0.8,height=0.5];
n7 [shape=component, label="8", fixedsize=true, width=0.4,height=0.4];
n8 [shape=component, label="9", fixedsize=true, width=0.4,height=0.4];
n9 [shape=ellipse, label="10", fixedsize=true, width=0.5,height=0.5];
n10 [shape=ellipse, label="11", fixedsize=true, width=0.5,height=0.5];
n11 [shape=component, label="12", fixedsize=true, width=0.4,height=0.4];
n12 [shape=component, label="13", fixedsize=true, width=0.4,height=0.4];
n13 [shape=ellipse, label="14", fixedsize=true, width=0.5,height=0.5];
n14 [shape=ellipse, label="15", fixedsize=true, width=0.5,height=0.5];
n0 -> n1;
n0 -> n2;
n1 -> n3;
n1 -> n4;
n2 -> n5;
n2 -> n6;
n3 -> n7;
n3 -> n8;
n4 -> n9;
n4 -> n10;
n5 -> n11;
n5 -> n12;
n6 -> n13;
n6 -> n14;
}
```
:::danger
總共需要 $1+2+...+2^{\lceil \log_2n\rceil}=2^{\lceil \log_2n\rceil+1}-1\lt 4n$ 個節點。
:::
> 別忘記我們從 $1$ 開始編號,所以實際存放線段樹的陣列長度要再加一,
> 當然也可以從 $0$ 開始編號。
> ~~不想算的話就直接開 $4n$ 的空間也可以~~
>
> $2^{\lceil \log_2n\rceil}$ 就是第一個不小於 $n$ 的 $2$ 的冪。
> 將最左邊為 $1$ 的位元的右邊位元全部變成 $1$,最後再加一就是答案了。
>
> ```cpp!
> unsigned power2ceil(unsigned n) {
> // smallest power of 2 >= n
> n -= 1;
> n |= n >> 1;
> n |= n >> 2;
> n |= n >> 4;
> n |= n >> 8;
> n |= n >> 16;
> return n + 1;
> }
> ```
>
> 概念如下,以 8 位元整數舉例。
>
> ```graphviz
> digraph {
> rankdir=LR;
> node [shape=record, style=filled];
> n0 [label="{0|0|0|1|?|?|?|?}"];
> n1 [label="{0|0|0|1|1|?|?|?}"];
> n2 [label="{0|0|0|1|1|1|1|?}"];
> n3 [label="{0|0|0|1|1|1|1|1}"];
>
> edge [arrowhead=tee];
> n0 -> n1 -> n2 -> n3;
> }
> ```
>
> 將第一個 $1$ 後的第一個位元變為 $1$,總共就有 $2$ 個 $1$ 了,再來是 $4$ 個,$8$ 個,以此類推。
<hr class="dashed">
## 實作(Implementation)
:::warning
給定一個長度為 $n$ 的整數陣列,需要支援二種操作:
* 將第 $p$ 個元素改為 $x$
* 回答 $[l:r)$ 的區間和
:::
> 資料結構可以寫成一個類別(class)會比較好,
> 當然要把資料都宣告成全域變數(global variables),
> 或是傳入一堆參數給函數也不是不行。
```cpp!
class SGT {
int n;
vector<long long> t; // tree
int left(int tv) { return 2 * tv; } // left child of tv
int right(int tv) { return 2 * tv + 1; } // right child of tv
void build(const vector<long long>&, int, int, int);
void set(int, long long, int, int, int);
long long sum(int, int, int, int, int);
public:
SGT(const vector<long long>& v) : n{v.size()}, t(2 * power2ceil(n)) {
build(v, 1, 0, n);
}
void set(int, long long);
long long sum(int, int);
};
```
<hr class="dotted">
## 建立(Build)
先把陣列一直對半分開,等子陣列長度為 $1$ 時,就初始為對應的元素,
之後遞迴返回之時,將子陣列的結果兩兩合併。
> * tv
> * tree vertex
> * 代表節點的編號
> * tl
> * tree left
> * 代表節點的子陣列的左邊界
> * tr
> * tree right
> * 代表節點的子陣列的右邊界
```cpp!
void SGT::build(const vector<long long>& v, int tv, int tl, int tr) {
if (tr - tl == 1) t[tv] = v[tl];
else {
int tm{(tl + tr) / 2};
build(v, left(tv), tl, tm);
build(v, right(tv), tm, tr);
t[tv] = t[left(tv)] + t[right(tv)];
}
}
```
每個節點都造訪一次,複雜度為 $O(n)$。
> ~~一般需要使用線段樹的題目,$O(n\log n)$ 的複雜度都是可以被接受的,
> 不用寫 `build()`,直接用 `set()` 把每個元素初始化一次就好。~~
<hr class="dotted">
## 單點修改(Modify)
> 修改也可以是把某個元素加上一個值,做法都一樣。
只需要更新與修改的元素有關的節點即可。
```cpp!
void SGT::set(int p, long long x, int tv, int tl, int tr) {
if (tr - tl == 1) t[tv] = x;
else {
int tm{(tl + tr) / 2};
if (p < tm) set(p, x, left(tv), tl, tm);
else set(p, x, right(tv), tm, tr);
t[tv] = t[left(tv)] + t[right(tv)];
}
}
```
因為每一層只有造訪一個節點,複雜度跟樹高一樣,為 $O(\log n)$。
```cpp!
void SGT::set(int p, long long x) {
set(p, x, 1, 0, n);
}
```
<hr class="dotted">
## 區間查詢(Range Query)
如果子區間與子陣列相等,就不需要繼續切分了,直接回傳預先算好的值。
```cpp!
long long SGT::sum(int l, int r, int tv, int tl, int tr) {
if (l == tl && r == tr) return t[tv];
int tm{(tl + tr) / 2};
if (r <= tm) return sum(l, r, left(tv), tl, tm);
else if (l >= tm) return sum(l, r, right(tv), tm, tr);
else return sum(l, tm, left(tv), tl, tm) + sum(tm, r, right(tv), tm, tr);
}
```
每一層最多造訪四個節點,複雜度為 $O(\log n)$。
> #### 證明
> 第一層只有樹根一個節點,命題為真;
> 假設第 $k$ 層命題為真,第 $k$ 層被造訪的節點,只有最左以及最右的節點可以再往下遞迴
> (因為一開始整個區間是連續的,中間節點的子區間與子陣列必須相等),
> 因此第 $k+1$ 層最多就四個節點被造訪,命題也為真;
> 根據數學歸納法,對任意層命題都為真。
```cpp!
long long SGT::sum(int l, int r) {
return sum(l, r, 1, 0, n);
}
```
<hr class="dashed">
## 空間優化(Memory optimization)[^CPAlgo]
前面的編碼方式,在 $n$ 不是 $2$ 的冪時,會浪費一些空間,
這邊透過不同的編碼方式,不管 $n$ 如何,都只需要 $2n-1$ 的空間。
一開始說過,$n$ 個元素需要 $2n-1$ 個節點,
因此右子樹編碼時,可以直接預留左子樹需要的空間,
只需要改一下 `left()` 與 `right()` 即可。
```cpp!
SGT::left(int tv) {
return tv + 1;
}
SGT::right(int tv, int tl, int tm) {
return tv + 2 * (tm - tl);
}
```
> 這樣的編碼就好像前序遍歷(pre-order traversal)。
```graphviz
digraph {
n0 [shape=ellipse, label="1", fixedsize=true, width=1.0,height=0.5];
n1 [shape=ellipse, label="2", fixedsize=true, width=0.9,height=0.5];
n2 [shape=ellipse, label="7", fixedsize=true, width=0.9,height=0.5];
n3 [shape=ellipse, label="3", fixedsize=true, width=0.5,height=0.5];
n4 [shape=ellipse, label="4", fixedsize=true, width=0.8,height=0.5];
n5 [shape=ellipse, label="8", fixedsize=true, width=0.5,height=0.5];
n6 [shape=ellipse, label="9", fixedsize=true, width=0.8,height=0.5];
n9 [shape=ellipse, label="5", fixedsize=true, width=0.5,height=0.5];
n10 [shape=ellipse, label="6", fixedsize=true, width=0.5,height=0.5];
n13 [shape=ellipse, label="10", fixedsize=true, width=0.5,height=0.5];
n14 [shape=ellipse, label="11", fixedsize=true, width=0.5,height=0.5];
n0 -> n1;
n0 -> n2;
n1 -> n3;
n1 -> n4;
n2 -> n5;
n2 -> n6;
//n3 -> n7;
//n3 -> n8;
n4 -> n9;
n4 -> n10;
//n5 -> n11;
//n5 -> n12;
n6 -> n13;
n6 -> n14;
}
```
<hr class="dashed">
## 適用問題
除了求區間和,還有什麼區間查詢可以呢?
* sum
* minimum / maximum
* gcd
* ...
:::danger
任何符合結合律(Associative property)的函數都可以使用線段樹。
:::
線段樹儲存的資料也不一定是整數,有可能是簡單的結構(struct)。
> 在 C++ 中,除了 `struct` 預設成員為公開,`struct` 與 `class` 基本上沒有差異;
> 有些人會習慣把 `struct` 拿來用在一些很簡單的類別。
> 如果是事先寫好的程式碼,可以透過模板(template),
> 來實現泛型編程(Generic programming),
> 除了最基本的可以切換 `int` 與 `long long`,
> 甚至是其它自己寫的結構,不過要有一些基本的運算子。
---
# 區間修改線段樹(Segment Tree with Range Modification)
> 如果現在不只單點修改,而是區間修改呢?
> 區間內每個元素都要改變的話,就要線性時間了,但想想以下二種情況,
> 第一種是修改了之後,某些部分根本沒有再被詢問到,
> 第二種是修改了之後,在被詢問到之前,又被二次修改了,
> 那這樣真的有必要馬上把全部的節點都更新嗎?
如果整個子陣列都被做一樣的修改,我們「通常」可以知道整個子陣列的結果會如何變化,
而這取決於修改函式與詢問函式是什麼。
以修改函式是最簡單的賦值(assignment),詢問函式是最大值(maximum)為例,
如果子陣列都改為 $x$ 的話,整個子陣列的最大值就是 $x$。
> 變更前的狀態:
>
> ```graphviz
> digraph {
> n0 [shape=ellipse, label="7,", fixedsize=true, width=1.0,height=0.5];
> n1 [shape=ellipse, label="7,", fixedsize=true, width=0.9,height=0.5];
> n2 [shape=ellipse, label="4,", fixedsize=true, width=0.9,height=0.5];
> n3 [shape=ellipse, label="5", fixedsize=true, width=0.5,height=0.5];
> n4 [shape=ellipse, label="7,", fixedsize=true, width=0.8,height=0.5];
> n5 [shape=ellipse, label="4", fixedsize=true, width=0.5,height=0.5];
> n6 [shape=ellipse, label="2,", fixedsize=true, width=0.8,height=0.5];
> n9 [shape=ellipse, label="7", fixedsize=true, width=0.5,height=0.5];
> n10 [shape=ellipse, label="3", fixedsize=true, width=0.5,height=0.5];
> n13 [shape=ellipse, label="1", fixedsize=true, width=0.5,height=0.5];
> n14 [shape=ellipse, label="2", fixedsize=true, width=0.5,height=0.5];
>
> n0 -> n1;
> n0 -> n2;
> n1 -> n3;
> n1 -> n4;
> n2 -> n5;
> n2 -> n6;
> //n3 -> n7;
> //n3 -> n8;
> n4 -> n9;
> n4 -> n10;
> //n5 -> n11;
> //n5 -> n12;
> n6 -> n13;
> n6 -> n14;
> }
> ```
>
> 將 $[1:4)$ 改為 0,變更後的狀態:
>
> ```graphviz
> digraph {
> n0 [shape=ellipse, label="5,", fixedsize=true, width=1.0,height=0.5];
> n1 [shape=ellipse, label="5,", fixedsize=true, width=0.9,height=0.5];
> n2 [shape=ellipse, label="2,", fixedsize=true, width=0.9,height=0.5];
> n3 [shape=ellipse, label="5", fixedsize=true, width=0.5,height=0.5];
> n4 [shape=ellipse, label="0,0", fixedsize=true, width=0.8,height=0.5];
> n5 [shape=ellipse, label="0", fixedsize=true, width=0.5,height=0.5];
> n6 [shape=ellipse, label="2,", fixedsize=true, width=0.8,height=0.5];
> n9 [shape=ellipse, label="7", fixedsize=true, width=0.5,height=0.5];
> n10 [shape=ellipse, label="3", fixedsize=true, width=0.5,height=0.5];
> n13 [shape=ellipse, label="1", fixedsize=true, width=0.5,height=0.5];
> n14 [shape=ellipse, label="2", fixedsize=true, width=0.5,height=0.5];
>
> n0 -> n1;
> n0 -> n2;
> n1 -> n3;
> n1 -> n4;
> n2 -> n5;
> n2 -> n6;
> //n3 -> n7;
> //n3 -> n8;
> n4 -> n9;
> n4 -> n10;
> //n5 -> n11;
> //n5 -> n12;
> n6 -> n13;
> n6 -> n14;
> }
> ```
這項技巧叫延遲傳遞(lazy propagation),當某個修改的子區間等於整個子陣列的話,
我們就把整個子陣列的結果算好,並記錄還沒往下修改的值是什麼(因為下面的節點還沒更新),
而暫時不去修改所有相關的節點。
> 這項技巧與寫入時複製(COW, Copy-on-write)的想法一樣,
> **等到某件事真的需要被完成的時候,我們才去完成它。**
<hr class=dashed>
## 實作(Implementation)
:::warning
給定一個長度為 $n$ 的整數陣列,需要支援二種操作:
* 將區間 $[l:r)$ 中的每個元素加上 $x$
* 回答 $[l:r)$ 的區間和
:::
> 修改函式是加(addition),詢問函式是和(sum)
> 這邊就用空間優化的編碼方式吧,
> 至於 `build()` 基本上一樣,就不寫了。
我們現在需要多一個陣列 `lz`,紀錄尚未往下更新的值,
> 這邊使用 `std::optional` 比較一般化(general),像是以這個問題為例,
> 沒有值要往下更新的時候,可以設為 $0$ 就好,畢竟加上 $0$ 沒有任何影響,
> 但如果修改函式是直接賦值的話,這樣就會有問題了。
```cpp!
template <typename value_t>
class SGT {
int n;
vector<value_t> t;
vector<optional<value_t>> lz;
int left(int tv) { return tv + 1; }
int right(int tv, int tl, int tm) { return tv + 2 * (tm - tl); }
value_t merge(const value_t&, const value_t&);
void update(int, int, const value_t&);
void push(int, int, int);
void modify(int, int, const value_t&, int, int, int);
value_t query(int, int, int, int, int);
public:
explicit SGT(int _n) : n{_n}, t(2 * n), lz(2 * n) {}
void modify(int l, int r, const value_t& x) { modify(l, r, x, 1, 0, n); }
value_t query(int l, int r) { return query(l, r, 1, 0, n); }
};
```
<hr class=dotted>
## `merge()`
因為是查詢函式是和,自然就是回傳 `x + y`。
```cpp!
template <typename value_t>
value_t SGT<value_t>::merge(const value_t& x, const value_t& y) {
return x + y;
}
```
<hr class=dotted>
## `update()`
當修改子區間與子陣列相等時,`update()` 會更新 `tv` 所在的節點,
並記錄還沒有往下更新的值。
因為每個元素都加上 `x`,整個子陣列的和自然就要加上 `len * x`。
> 有可能整個區間之前就被加過某個 $x'$ 了,
> 記得不能把舊的值覆蓋掉,它們也還沒往下更新。
```cpp!
template <typename value_t>
void SGT<value_t>::update(int tv, int len, const value_t& x) {
if (!lz[tv]) lz[tv] = x;
else lz[tv] = lz[tv].value() + x;
t[tv] += len * x;
}
```
<hr class=dotted>
## `push()`
`push` 將儲存在 `lz` 中的值往下一層更新,並將 `lz` 初始。
```cpp!
template <typename value_t>
void SGT<value_t>::push(int tv, int tl, int tr) { // lazy propagation
if (!lz[tv]) return;
int tm{(tl + tr) / 2};
update(left(tv), tm - tl, lz[tv].value());
update(right(tv, tl, tm), tr - tm, lz[tv].value());
lz[tv].reset();
}
```
<hr class=dotted>
## `modify()`
整個流程與查詢很像,如果子區間與子陣列相等,就透過 `update()` 去更新,
否則就只能把要修改的子區間繼續拆分。
既然要繼續造訪下層節點,那就必須把之前沒有更新的值,先更新好,才能繼續前進,
因此透過 `push()` 把需要更新的值往下一層推進。
```cpp!
template <typename value_t>
void SGT<value_t>::modify(int l, int r, const value_t& x, int tv, int tl, int tr) {
if (l == tl && r == tr) update(tv, tr - tl, x);
else {
push(tv, tl, tr);
int tm{(tl + tr) / 2};
if (r <= tm) modify(l, r, x, left(tv), tl, tm);
else if (l >= tm) modify(l, r, x, right(tv, tl, tm), tm, tr);
else modify(l, tm, x, left(tv), tl, tm),
modify(tm, r, x, right(tv, tl, tm), tm, tr);
t[tv] = merge(t[left(tv)], t[right(tv, tl, tm)]);
}
}
```
<hr class=dotted>
## `query()`
**別忘了先把沒更新的值先往下更新**,其餘流程都一樣。
```cpp!
template <typename value_t>
value_t SGT<value_t>::query(int l, int r, int tv, int tl, int tr) {
if (l == tl && r == tr) return t[tv];
push(tv, tl, tr);
int tm{(tl + tr) / 2};
if (r <= tm) return query(l, r, left(tv), tl, tm);
else if (l >= tm) return query(l, r, right(tv, tl, tm), tm, tr);
else return merge(query(l, tm, left(tv), tl, tm),
query(tm, r, right(tv, tl, tm), tm, tr));
}
```
<hr class=dotted>
**只有 `merge()` 跟 `update()` 會隨著不同的修改與查詢改變。**
> 而像是修改函式是加,而詢問函式是最大公因數的話,
> 就沒辦法透過延遲傳遞(lazy propagation)來快速進行。
:::spoiler 完整程式碼
```cpp!
// segment tree
// range query & range modify
template <typename value_t>
class SGT {
int n;
vector<value_t> t;
vector<optional<value_t>> lz;
// [ tv+1 : tv+2*(tm-tl) ) -> left subtree
int left(int tv) { return tv + 1; }
int right(int tv, int tl, int tm) { return tv + 2 * (tm - tl); }
/** differ from case to case **/
// query is "sum" and modify is "add" here
value_t merge(const value_t& x, const value_t& y) { // associative function
return x + y;
}
void update(int tv, int len, const value_t& x) {
if (!lz[tv]) lz[tv] = x;
else lz[tv] = lz[tv].value() + x;
t[tv] += len * x;
}
/******************************/
void push(int tv, int tl, int tr) { // lazy propagation
if (!lz[tv]) return;
int tm{(tl + tr) / 2};
update(left(tv), tm - tl, lz[tv].value());
update(right(tv, tl, tm), tr - tm, lz[tv].value());
lz[tv].reset();
}
void modify(int l, int r, const value_t& x, int tv, int tl, int tr) {
if (l == tl && r == tr) update(tv, tr - tl, x);
else {
push(tv, tl, tr);
int tm{(tl + tr) / 2};
if (r <= tm) modify(l, r, x, left(tv), tl, tm);
else if (l >= tm) modify(l, r, x, right(tv, tl, tm), tm, tr);
else modify(l, tm, x, left(tv), tl, tm),
modify(tm, r, x, right(tv, tl, tm), tm, tr);
t[tv] = merge(t[left(tv)], t[right(tv, tl, tm)]);
}
}
value_t query(int l, int r, int tv, int tl, int tr) {
if (l == tl && r == tr) return t[tv];
push(tv, tl, tr);
int tm{(tl + tr) / 2};
if (r <= tm) return query(l, r, left(tv), tl, tm);
else if (l >= tm) return query(l, r, right(tv, tl, tm), tm, tr);
else return merge(query(l, tm, left(tv), tl, tm),
query(tm, r, right(tv, tl, tm), tm, tr));
}
public:
explicit SGT(int _n) : n{_n}, t(2 * n), lz(2 * n) {}
void modify(int l, int r, const value_t& x) { modify(l, r, x, 1, 0, n); } // [l:r)
value_t query(int l, int r) { return query(l, r, 1, 0, n); } // [l:r)
};
```
:::
<br>
**記得要對哪邊操作,就把之前懶得更新的部分也先往下推進**,
除此之外,區間修改線段樹與基本線段樹沒什麼差別,就看有沒有區間修改的需求。
---
# 極簡線段樹(Compact Segment Tree)[^EESGT]:bulb:
> :bulb::bulb::bulb:[原文](https://codeforces.com/blog/entry/18051)稱之為「Efficient and easy segment trees」;
> 中文圈也有提出類似(一樣?)的資料結構稱之為「zkw 線段樹」。
陣列長度 $32$ 的線段樹,查詢區間為 $[5:29)$:
```graphviz
digraph {
SGT [shape=none, label=
<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="8" CELLPADDING="0">
<TR>
<TD COLSPAN="64" BGCOLOR="Red"> 1:<BR/>[0:32)</TD>
</TR>
<TR>
<TD COLSPAN="32" BGCOLOR="Orange"> 2:<BR/>[0:16)</TD>
<TD COLSPAN="32"> 3:<BR/>[16:32)</TD>
</TR>
<TR>
<TD COLSPAN="16" BGCOLOR="Yellow"> 4:<BR/>[0:8)</TD>
<TD COLSPAN="16" SIDES="LT" BORDER="5"> 5:<BR/>[8:16)</TD>
<TD COLSPAN="16" SIDES="TR" BORDER="5"> 6:<BR/>[16:24)</TD>
<TD COLSPAN="16"> 7:<BR/>[24:32)</TD>
</TR>
<TR>
<TD COLSPAN="8" BGCOLOR="Green"> 8:<BR/>[0:4)</TD>
<TD COLSPAN="8"> 9:<BR/>[4:8)</TD>
<TD COLSPAN="8" SIDES="L" BORDER="5"> 10:<BR/>[8:12)</TD>
<TD COLSPAN="8" BORDER="0"> 11:<BR/>[12:16)</TD>
<TD COLSPAN="8" BORDER="0"> 12:<BR/>[16:20)</TD>
<TD COLSPAN="8" BORDER="0"> 13:<BR/>[20:24)</TD>
<TD COLSPAN="8" SIDES="TR" BORDER="5"> 14:<BR/>[24:28)</TD>
<TD COLSPAN="8"> 15:<BR/>[28:32)</TD>
</TR>
<TR>
<TD COLSPAN="4" BGCOLOR="Blue"> 16:<BR/>[0:2)</TD>
<TD COLSPAN="4"> 17:<BR/>[2:4)</TD>
<TD COLSPAN="4"> 18:<BR/>[4:6)</TD>
<TD COLSPAN="4" SIDES="LT" BORDER="5"> 19:<BR/>[6:8)</TD>
<TD COLSPAN="4" BORDER="0"> 20:<BR/>[8:10)</TD>
<TD COLSPAN="4" BORDER="0"> 21:<BR/>[10:12)</TD>
<TD COLSPAN="4" BORDER="0"> 22:<BR/>[12:14)</TD>
<TD COLSPAN="4" BORDER="0"> 23:<BR/>[14:16)</TD>
<TD COLSPAN="4" BORDER="0"> 24:<BR/>[16:18)</TD>
<TD COLSPAN="4" BORDER="0"> 25:<BR/>[18:20)</TD>
<TD COLSPAN="4" BORDER="0"> 26:<BR/>[20:22)</TD>
<TD COLSPAN="4" BORDER="0"> 27:<BR/>[22:24)</TD>
<TD COLSPAN="4" BORDER="0"> 28:<BR/>[24:26)</TD>
<TD COLSPAN="4" SIDES="R" BORDER="5"> 29:<BR/>[26:28)</TD>
<TD COLSPAN="4"> 30:<BR/>[28:30)</TD>
<TD COLSPAN="4"> 31:<BR/>[30:32)</TD>
</TR>
<TR>
<TD COLSPAN="2" BGCOLOR="Purple">32:<BR/>0</TD>
<TD COLSPAN="2">33:<BR/>1</TD>
<TD COLSPAN="2">34:<BR/>2</TD>
<TD COLSPAN="2">35:<BR/>3</TD>
<TD COLSPAN="2">36:<BR/>4</TD>
<TD COLSPAN="2" SIDES="LTB" BORDER="5">37:<BR/>5</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">38:<BR/>6</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">39:<BR/>7</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">40:<BR/>8</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">41:<BR/>9</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">42:<BR/>10</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">43:<BR/>11</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">44:<BR/>12</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">45:<BR/>13</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">46:<BR/>14</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">47:<BR/>15</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">48:<BR/>16</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">49:<BR/>17</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">50:<BR/>18</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">51:<BR/>19</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">52:<BR/>20</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">53:<BR/>21</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">54:<BR/>22</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">55:<BR/>23</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">56:<BR/>24</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">57:<BR/>25</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">58:<BR/>26</TD>
<TD COLSPAN="2" SIDES="B" BORDER="5">59:<BR/>27</TD>
<TD COLSPAN="2" SIDES="TRB" BORDER="5">60:<BR/>28</TD>
<TD COLSPAN="2">61:<BR/>29</TD>
<TD COLSPAN="2">62:<BR/>30</TD>
<TD COLSPAN="2">63:<BR/>31</TD>
</TR>
</TABLE>>
];
}
```
長度為 $2$ 的冪的陣列建構而成的線段樹,本身就是一棵完美二元樹,
規律性也相當完美,陣列元素會連續地放在 $[n:2n)$,
其餘同一層(代表的子陣列長度相同)的節點,也都是連續擺放的;
這代表我們並不需要每次都往下遞迴,而是可以直接從底層開始。
:::danger
區間 $[l:r)$ 對應的節點在 $[n+l,n+r)$。
:::
> 這邊假設函式有交換律(Commutative property);
> 如果沒有的話,也稍微進行修改就可以了。
* `build()`
* 節點編號從大到小,就可以確保子節點都先初始
* `modify()`
* 從樹葉出發,一直往父節點移動
* `query`
* 從一堆樹葉出發,如果左右子節點都在區間內,就繼續往上;否則就要加入答案之中
> `<< 1` 等於 `* 2`, `>> 1` 等於 `/ 2`
> 模板的設計上跟 `std::priority_queue` 很像。
```cpp!
template<typename value_t, class merge_t>
class SGT {
int n;
vector<value_t> t; // root starts at 1
merge_t merge; // associative function for SGT
public:
explicit SGT(int _n = 0, const merge_t& _merge = merge_t{})
: n{_n}, t(2 * n), merge{_merge} {}
void build(const vector<value_t>& v) {
n = v.size(), t.resize(2 * n);
for (int p{2 * n - 1}; p >= n; --p) t[p] = v[p - n];
for (int p{n - 1}; p >= 0; --p) t[p] = merge(t[p << 1], t[(p << 1) + 1]);
}
void modify(int p, const value_t& x) {
for (t[p += n] = x; p > 1; p >>= 1) t[p >> 1] = merge(t[p], t[p ^ 1]);
}
value_t query(int l, int r, value_t init) { // [l:r)
for (l += n, r += n; l < r; l >>= 1, r >>= 1) {
if (l & 1) init = merge(init, t[l++]);
if (r & 1) init = merge(init, t[--r]);
}
return init;
}
};
```
> `query()` 的 `init` 是為了讓程式碼較精簡,也可用 `std::optional` 取代。
> 詢問函式為和時,`init` 給 $0$;詢問函式為最大值時,`init` 給一個極小值,
> 或是如果元素都是負值,希望答案為 $0$,也可以把 `init` 給 $0$。
可能讀者覺得好像沒有很精簡阿?我們仔細來看看,
`build()` 三行,`modify()` 一行,`query` 五行,
從遞迴改為迴圈,竟然還只剩下**九行程式碼**!
也不用像之前一樣有四個五個的參數。
> 不過它主要適合單點修改區間查詢的操作,快速並容易實作,
> 缺點就是沒什麼拓展性,例如要做區間修改就會變得很麻煩。
<hr class="dashed">
神奇的是,在長度不是 $2$ 的冪時,完全一樣的程式碼還是可以正常運作,
基本上節點還是維持著一層一層的架構,沒有用(灰色)的節點就不管它們。
> 改到了也沒關係,因為完全不會用到它們的值。
陣列長度 $37$ 的線段樹,查詢區間為 $[5:29)$:
```graphviz
digraph {
SGT [shape=none, label=
<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="8" CELLPADDING="0">
<TR>
<TD COLSPAN="64" BGCOLOR="GhostWhite" BORDER="0"> 1:<BR/>-</TD>
</TR>
<TR>
<TD COLSPAN="32" BGCOLOR="GhostWhite" BORDER="0"> 2:<BR/>-</TD>
<TD COLSPAN="32" BGCOLOR="Coral" SIDES="LTR" BORDER="5"> 3:<BR/>[11:27)</TD>
</TR>
<TR>
<TD COLSPAN="16" BGCOLOR="GhostWhite" BORDER="0"> 4:<BR/>-</TD>
<TD COLSPAN="16" BGCOLOR="Gold"> 5:<BR/>[3:11)</TD>
<TD COLSPAN="16" BGCOLOR="Gold" SIDES="L" BORDER="5"> 6:<BR/>[11:19)</TD>
<TD COLSPAN="16" BGCOLOR="Gold" SIDES="R" BORDER="5"> 7:<BR/>[19:27)</TD>
</TR>
<TR>
<TD COLSPAN="8" BGCOLOR="Gold"> 8:<BR/>[27:35)</TD>
<TD COLSPAN="8" BGCOLOR="GhostWhite" BORDER="0"> 9:<BR/>-</TD>
<TD COLSPAN="8" BGCOLOR="LawnGreen">10:<BR/>[3:7)</TD>
<TD COLSPAN="8" BGCOLOR="LawnGreen" SIDES="LT" BORDER="5">11:<BR/>[7:11)</TD>
<TD COLSPAN="8" BGCOLOR="LawnGreen" BORDER="0">12:<BR/>[11:15)</TD>
<TD COLSPAN="8" BGCOLOR="LawnGreen" BORDER="0">13:<BR/>[15:19)</TD>
<TD COLSPAN="8" BGCOLOR="LawnGreen" BORDER="0">14:<BR/>[19:23)</TD>
<TD COLSPAN="8" BGCOLOR="LawnGreen" SIDES="R" BORDER="5">15:<BR/>[23:27)</TD>
</TR>
<TR>
<TD COLSPAN="4" BGCOLOR="LawnGreen"> 16:<BR/>[27:31)</TD>
<TD COLSPAN="4" BGCOLOR="LawnGreen"> 17:<BR/>[31:35)</TD>
<TD COLSPAN="4" BGCOLOR="GhostWhite" BORDER="0"> 18:<BR/>-</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue"> 19:<BR/>[1:3)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue"> 20:<BR/>[3:5)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" SIDES="LT" BORDER="5"> 21:<BR/>[5:7)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 22:<BR/>[7:9)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 23:<BR/>[9:11)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 24:<BR/>[11:13)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 25:<BR/>[13:15)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 26:<BR/>[15:17)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 27:<BR/>[17:19)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 28:<BR/>[19:21)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 29:<BR/>[21:23)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 30:<BR/>[23:25)</TD>
<TD COLSPAN="4" BGCOLOR="DeepSkyBlue" BORDER="0"> 31:<BR/>[25:27)</TD>
</TR>
<TR>
<TD COLSPAN="2" BGCOLOR="DeepSkyBlue" SIDES="TR" BORDER="5">32:<BR/>[27:29)</TD>
<TD COLSPAN="2" BGCOLOR="DeepSkyBlue">33:<BR/>[29:31)</TD>
<TD COLSPAN="2" BGCOLOR="DeepSkyBlue">34:<BR/>[31:33)</TD>
<TD COLSPAN="2" BGCOLOR="DeepSkyBlue">35:<BR/>[33:35)</TD>
<TD COLSPAN="2" BGCOLOR="DeepSkyBlue">36:<BR/>[35:37)</TD>
<TD COLSPAN="2" BGCOLOR="Violet">37:<BR/>0</TD>
<TD COLSPAN="2" BGCOLOR="Violet">38:<BR/>1</TD>
<TD COLSPAN="2" BGCOLOR="Violet">39:<BR/>2</TD>
<TD COLSPAN="2" BGCOLOR="Violet">40:<BR/>3</TD>
<TD COLSPAN="2" BGCOLOR="Violet">41:<BR/>4</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="LB" BORDER="5">42:<BR/>5</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">43:<BR/>6</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">44:<BR/>7</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">45:<BR/>8</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">46:<BR/>9</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">47:<BR/>10</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">48:<BR/>11</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">49:<BR/>12</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">50:<BR/>13</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">51:<BR/>14</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">52:<BR/>15</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">53:<BR/>16</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">54:<BR/>17</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">55:<BR/>18</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">56:<BR/>19</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">57:<BR/>20</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">58:<BR/>21</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">59:<BR/>22</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">60:<BR/>23</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">61:<BR/>24</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">62:<BR/>25</TD>
<TD COLSPAN="2" BGCOLOR="Violet" SIDES="B" BORDER="5">63:<BR/>26</TD>
</TR>
<TR>
<TD COLSPAN="1" BGCOLOR="Violet" SIDES="B" BORDER="5">64:<BR/>27</TD>
<TD COLSPAN="1" BGCOLOR="Violet" SIDES="RB" BORDER="5">65:<BR/>28</TD>
<TD COLSPAN="1" BGCOLOR="Violet">66:<BR/>29</TD>
<TD COLSPAN="1" BGCOLOR="Violet">67:<BR/>30</TD>
<TD COLSPAN="1" BGCOLOR="Violet">68:<BR/>31</TD>
<TD COLSPAN="1" BGCOLOR="Violet">69:<BR/>32</TD>
<TD COLSPAN="1" BGCOLOR="Violet">70:<BR/>33</TD>
<TD COLSPAN="1" BGCOLOR="Violet">71:<BR/>34</TD>
<TD COLSPAN="1" BGCOLOR="Violet">72:<BR/>35</TD>
<TD COLSPAN="1" BGCOLOR="Violet">73:<BR/>36</TD>
</TR>
</TABLE>>
];
}
```
> 只看有用的點的話,它也不算一個樹,而是一個深林。
詳細證明可以參考 ["Efficient and easy segment trees" proof](https://codeforces.com/blog/entry/89313)。
---
# 線段樹應用(Applications of Segment Tree)
### [逆序數對(Inversions)](https://codeforces.com/edu/course/2/lesson/4/3/practice/contest/274545/problem/A)
:::info
給定一個長度為 $n$ 的陣列,元素最大不超過 $n$,
對於每一個 $i$,請找出有幾個 $j$,使得 $j\lt i$ 且 $a_j\gt a_i$。
:::
建立一個 $cnt$ 陣列,$cnt[i]$ 代表數字 $i$ 出現的次數,
這樣某個範圍的總和,就等於是這之間的數字個數,
而區間查詢我們可以透過線段樹來快速計算。
```cpp!
const int MaxValue{n};
SGT<int, plus<int>> sgt{};
sgt.build(vector<int>(MaxValue + 1, 0));
vector<int> inv(n);
for (int i{0}; i < n; ++i) {
inv[i] = sgt.query(v[i] + 1, MaxValue + 1, 0);
sgt.modify(v[i], sgt.query(v[i], v[i] + 1, 0) + 1);
}
```
<hr class="dashed">
### [最大連續和(Maximum Subarray)](https://codeforces.com/edu/course/2/lesson/4/2/practice/contest/273278/problem/A)
:::info
給定一個長度為 $n$ 的陣列,支援下列二種操作:
* 將第 $i$ 個元素改為 $x$
* 詢問區間 $[l:r)$ 中的最大連續和
:::
[前面](https://hackmd.io/@XDEv11/NCKU-AdvCP-2021-WayOfThinking#%E5%88%86%E6%B2%BB%EF%BC%88Divide-and-Conquer%EF%BC%89)最大連續和時說過,最大連續和等於左邊的最大連續和,
右邊的最大連續和,或橫跨二邊的最大連續和。
但我們不想要每次都去重新計算橫跨二邊的最大連續和,
因此對於每個子區間,我們多紀錄最大前綴和與最大後綴和,
左邊子區間最大後綴和加上右邊子區間最大前綴和,即是橫跨二邊的最大連續和。
```cpp!
struct value_t {
long long sum, pre, suf, mx;
value_t()=default;
value_t(long long x) : sum{x}, pre{x}, suf{x}, mx{x} {}
};
struct merge_t {
value_t operator()(const value_t& x, const value_t& y) {
value_t res{};
res.sum = x.sum + y.sum;
res.pre = max(x.pre, x.sum + y.pre);
res.suf = max(x.suf + y.sum, y.suf);
res.mx = max(max(x.mx, y.mx), x.suf + y.pre);
return res;
}
};
```
> :bulb:這邊寫了一個 [Converting constructor](https://en.cppreference.com/w/cpp/language/converting_constructor),讓它可以進行隱式轉換(Implicit conversions)。
接著就可以直接套用線段樹的模板了。
此合併函式不具有交換律,須注意運算元的前後順序,
所以這題使用基本線段樹,需要考慮的問題會少一點,
不過這邊示範如何使用極簡線段樹來實現。
> `init` 給 $0$,在這邊可視為在詢問區間 $[l:r)$ 前後各加上一個 $0$,
> 如果 $[l:r)$ 的最大連續和為負值,答案剛好變為 $0$,也就是空區間的答案,
> 否則不影響結果(`mx` 的部分);
> `init` 如果給一個很小的值,則完全不影響結果。
:::spoiler 完整程式碼
```cpp!
#include <iostream>
#include <vector>
using namespace std;
template<typename value_t, class merge_t>
class SGT {
int n;
vector<value_t> t; // root starts at 1
merge_t merge; // associative function for SGT
public:
explicit SGT(int _n = 0, const merge_t& _merge = merge_t{})
: n{_n}, t(2 * n), merge{_merge} {}
void modify(int p, const value_t& x) {
for (t[p += n] = x; p > 1; p >>= 1)
if (p & 1) t[p >> 1] = merge(t[p - 1], t[p]);
else t[p >> 1] = merge(t[p], t[p + 1]);
}
value_t query(int l, int r, value_t init) { // [l:r)
value_t L{init}, R{init};
for (l += n, r += n; l < r; l >>= 1, r >>= 1) {
if (l & 1) L = merge(L, t[l++]);
if (r & 1) R = merge(t[--r], R);
}
return merge(L, R);
}
};
void solve() {
int n, m;
cin >> n >> m;
struct value_t {
long long sum, pre, suf, mx;
value_t()=default;
value_t(long long x) : sum{x}, pre{x}, suf{x}, mx{x} {}
};
struct merge_t {
value_t operator()(const value_t& x, const value_t& y) {
value_t res{};
res.sum = x.sum + y.sum;
res.pre = max(x.pre, x.sum + y.pre);
res.suf = max(x.suf + y.sum, y.suf);
res.mx = max(max(x.mx, y.mx), x.suf + y.pre);
return res;
}
};
SGT<value_t, merge_t> sgt{n};
for (int i{0}; i < n; ++i) {
long long x;
cin >> x;
sgt.modify(i, x);
}
cout << sgt.query(0, n, 0).mx << '\n';
while (m--) {
int i; long long x;
cin >> i >> x;
sgt.modify(i, x);
cout << sgt.query(0, n, 0).mx << '\n';
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t{1};
// cin >> t;
while (t--) solve();
return 0;
}
```
:::
<hr class="dashed">
### [第 $k$ 個 $1$(K-th one)](https://codeforces.com/edu/course/2/lesson/4/2/practice/contest/273278/problem/B)
:::info
給定一個長度為 $n$ 的陣列,元素為 $0$ 或 $1$,支援下列二種操作:
* 將第 $i$ 個元素反轉($0\to 1,1\to 0$)
* 詢問第 $k$ 個 $1$ 的位置
:::
「第 $k$ 個 $1$ 的位置」這個問題好像不知道如何下手,
考慮一個更一般化的問題,「第一個前綴和大於等於 $x$ 的位置」。
先建立一棵合併函式為和的線段樹,我們就可以在線段樹上進行二分搜,
如果左子陣列的和大於等於 $ps$,代表答案在左邊,否則答案則在右邊。
> 記得先確認整個子陣列的和有大於 $ps$,之後就可以保證答案一定在 $[tl:tr)$ 之間。
```cpp!
int ps_lower_bound(int ps) { // prefix sum lower bound
if (t[1] < ps) return n;
int tv{1}, tl{0}, tr{n};
while (tr - tl > 1) {
int tm{(tl + tr) / 2};
if (t[left(tv)] >= ps) tv = left(tv), tr = tm;
else ps -= t[left(tv)], tv = right(tv, tl, tm), tl = tm;
}
return tl;
}
```
:::spoiler 完整程式碼
```cpp!
class SGT {
int n;
vector<int> t;
int left(int tv) { return tv + 1; }
int right(int tv, int tl, int tm) { return tv + 2 * (tm - tl); }
void build(const vector<int>& v, int tv, int tl, int tr) {
if (tr - tl == 1) t[tv] = v[tl];
else {
int tm{(tl + tr) / 2};
build(v, left(tv), tl, tm);
build(v, right(tv, tl, tm), tm, tr);
t[tv] = t[left(tv)] + t[right(tv, tl, tm)];
}
}
void flip(int p, int tv, int tl, int tr) {
if (tr - tl == 1) t[tv] = (t[tv] == 0 ? 1 : 0);
else {
int tm{(tl + tr) / 2};
if (p < tm) flip(p, left(tv), tl, tm);
else flip(p, right(tv, tl, tm), tm, tr);
t[tv] = t[left(tv)] + t[right(tv, tl, tm)];
}
}
public:
SGT(const vector<int>& v) : n{v.size()}, t(2 * n) {
build(v, 1, 0, n);
}
void flip(int p) {
flip(p, 1, 0, n);
};
int ps_lower_bound(int ps) { // prefix sum lower bound
if (t[1] < ps) return n;
int tv{1}, tl{0}, tr{n};
while (tr - tl > 1) {
int tm{(tl + tr) / 2};
if (t[left(tv)] >= ps) tv = left(tv), tr = tm;
else ps -= t[left(tv)], tv = right(tv, tl, tm), tl = tm;
}
return tl;
}
};
```
:::
<br>
> 透過適當的合併函式,我們一樣可以在線段樹上進行二分搜,找到不同問題的答案,
> 例如合併函式為最大值,我們可以找到「第一個大於 $x$ 的元素位置」。
---
# 稀疏表(Sparse Table)
> 前面說到不能根據 $[l:r)$ 對半切分,因為有太多不同的子區間的話,
> 想要事先計算就會變得不切實際,空間時間複雜度都會很高。
考慮區間最小值查詢(RMQ, Range Minimum Query),
會發現子問題的區間可以是重疊的,畢竟一個值被多考慮幾次,
也不會影響最小值是什麼。
所以我們可以讓子區間的長度有一定特性,
這樣不同查詢有可能出現的子區間數量就可以被限制住。
```graphviz
digraph {
SGT [shape=none, label=
<<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="2" CELLPADDING="4">
<TR>
<TD COLSPAN="8" BGCOLOR="LightCyan" HEIGHT="15"></TD>
<TD COLSPAN="5"></TD>
</TR>
<TR>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray">l</TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray">l+1</TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray">l+2</TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray"></TD>
<TD COLSPAN="1" BORDER="1" WIDTH="30" BGCOLOR="LightGray">r-1</TD>
</TR>
<TR>
<TD COLSPAN="5"></TD>
<TD COLSPAN="8" BGCOLOR="Pink" HEIGHT="15"></TD>
</TR>
</TABLE>>
];
}
```
如果子區間的長度都是 $2$ 的冪,將區間分成 $[l:l+2^{\lfloor\log_2(r-l)\rfloor})$
與 $[r-2^{\lfloor\log_2(r-l)\rfloor}:r)$,足以涵蓋整個區間,
所以事先計算每個點開始,長度為 $2$ 的冪的區間最小值。
<hr class="dashed">
## 實作(Implementation)
令 $st[i][j]$ 等於 $[i,i+2^j)$ 的最小值, $[i,i+2^j)$ 可以分半,
也就等於 $[i,i+2^{j-1})$ 與 $[i+2^{j-1},i+2^j)$ 二個區間的最小值。
:::success
$st[i][j]=\min(st[i][j-1],st[i+2^{j-1}][j-1])$
:::
令 $log2[i]$ 代表 $\lfloor\log_2i\rfloor$。
> 若 $k$ 為偶數,$k=2n$,
>
> $2^{log2[n]}\le n\lt 2^{log2[n]+1}$
> $\to 2^{log2[n]+1}\le 2n\lt 2^{log2[n]+2}$
>
> 則 $log2[k]=log2[k/2]+1$。
>
> 若 $k$ 為奇數,$k=2n+1$,
>
> $2^{log2[n]}\le n\lt 2^{log2[n]+1}$
> $\to 2^{log2[n]}\le n\le 2^{log2[n]+1}-1$
> $\to 2^{log2[n]+1}\le 2n\le 2^{log2[n]+2}-2$
> $\to 2^{log2[n]+1}\le 2n+1\lt 2^{log2[n]+2}$
>
> 則 $log2[k]=log2[k/2]+1$。
:::success
$log2[i]=log2[i/2]+1$
:::
```cpp!
vector<int> log2(n + 1);
log2[1] = 0;
for (int i{2}; i <= n; ++i) log2[i] = log2[i / 2] + 1;
vector<vector<int>> st(n, vector<int>(log2[n] + 1));
for (int i{n - 1}; i >= 0; --i) {
st[i][0] = v[i];
for (int j{1}; i + (1 << j) <= n; ++j)
st[i][j] = min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
```
建表複雜度為 $O(n\log n)$。
```cpp!
int j{log2[r - l]};
min(st[l][j], st[r - (1 << j)][j]);
```
查詢複雜度為 $O(1)$。
稀疏表的威力就在於 $O(1)$ 的最小值查詢,只可惜它不能支援快速的修改。
:::spoiler 完整程式碼
```cpp!
// sparse table
template<typename value_t, class merge_t>
class ST {
int n;
vector<int> log2;
vector<vector<value_t>> t;
merge_t merge; // associative & idempotent function for ST
public:
explicit ST(const vector<value_t>& v, const merge_t& _merge = merge_t{})
: n{v.size()}, log2(n + 1), merge{_merge} {
log2[1] = 0;
for (int i{2}; i <= n; ++i) log2[i] = log2[i / 2] + 1;
t.assign(n, vector<value_t>(log2[n] + 1));
for (int i{n - 1}; i >= 0; --i) {
t[i][0] = v[i];
for (int j{1}; i + (1 << j) <= n; ++j)
t[i][j] = merge(t[i][j - 1], t[i + (1 << (j - 1))][j - 1]);
}
}
value_t query(int l, int r) { // [l:r)
int j{log2[r - l]};
return merge(t[l][j], t[r - (1 << j)][j]);
}
};
```
:::
---
# References
* Ellis Horowitz, Sartaj Sahni, Susan Anderson-Freed (2007). "Fundamentals of Data Structures in C, 2/e".
* CP-Algorithms - [Segment Tree](https://cp-algorithms.com/data_structures/segment_tree.html)
* [Codeforces ITMO Academy: pilot course » Segment Tree](https://codeforces.com/edu/course/2/lesson/4)
* [Efficient and easy segment trees](https://codeforces.com/blog/entry/18051)
* ["Efficient and easy segment trees" proof](https://codeforces.com/blog/entry/89313)
* CP-Algorithms - [Sparse Table](https://cp-algorithms.com/data_structures/sparse-table.html)
* Algorithmica - [Algorithms for Modern Hardware 11.3 Segment trees](https://en.algorithmica.org/hpc/data-structures/segment-trees/)
---
# 練習題
| Problem | Tags |
|:-:|:- |
| [Xenia and Bit Operations](https://codeforces.com/contest/339/problem/D) | `segment tree` |
| [Pashmak and Parmida's problem](https://codeforces.com/contest/459/problem/D) | `segment tree` |
| [Petya and Array](https://codeforces.com/contest/1042/problem/D) | `segment tree` |
| [Flip](https://codeforces.com/gym/103373/problem/F) | `segment tree`, `range modification` |
| [區間 MAX](https://zerojudge.tw/ShowProblem?problemid=d539) | `sparse table` |
[^CPAlgo]: CP-Algorithms - [Segment Tree, Memory efficient implementation](https://cp-algorithms.com/data_structures/segment_tree.html#toc-tgt-6)
[^EESGT]: [Efficient and easy segment trees](https://codeforces.com/blog/entry/18051)
<style>
hr.thin {
height: 0.5px;
}
hr.dotted {
border-top: dotted;
height: .0em;
color: #777777;
background-color: #ffffff;
}
hr.dashed {
border-top: dashed;
height: .0em;
color: #777777;
background-color: #ffffff;
}
/* Gradient transparent - color - transparent */
hr.gradient {
border: 0;
height: 1px;
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
}
/* Flaired edges, by Tomas Theunissen */
hr.flaired {
overflow: visible;
height: 30px;
border-style: solid;
border-color: black;
border-width: 1px 0 0 0;
border-radius: 20px;
background-color: #ffffff;
}
hr.flaired:before { /* Not really supposed to work, but does */
display: block;
content: "";
height: 30px;
margin-top: -31px;
border-style: solid;
border-color: black;
border-width: 0 0 1px 0;
border-radius: 20px;
}
</style>