---
tags: 成大高階競技程式設計 2020, ys
---
:+1: [2020 競技程設教材 HackMD Book](https://hackmd.io/@nckuacm/ryLIV6BYI) :+1:
# 2020 Week 9: Data Structures
本篇將介紹些 CP 值較高的資料結構
> 其中 C 代表的是 coding 複雜度,~~或把 CP 譯作競程~~
# Sparse Table: RMQ
RMQ 全名 Range Maximum/Minimum Query
:::info
給定長度 $n$ 的數列 $a$,查詢**任意區間**極(最大/最小)值
:::
> 樸素的枚舉區間會有 $O(n^2)$ 的查詢複雜度
不失一般性,下面只以找**最大值**為例
定義狀態 $ST(i, k)$ 表示數列從 $i$ 位置開始,長度 $2^k$ 區間 $[i, i+2^k)$ 所有數中最大值
> 例如 $a = (1, 2, 5, 7, 2)$ 則 $ST(1, 1) = 5$,位置從 $0$ 開始
最大值可從**更小**的長度 $2^{k-1}$ 兩個區間比較得來,起點分別在 $i$ 與 $i+2^{k-1}$
則狀態轉移為 $ST(i, k) = \max(ST(i, k-1), ST(i+2^{k-1}, k-1))$
而邊界為長度 $1 = 2^0$ 區間 $ST(i, 0) = a_i$
```cpp
for(int i = 0; i < n; i++) ST[i][0] = a[i];
for(int k = 1; k <= log2(n); k++)
for(int i = 0; i+(1<<k) <= n; i++)
ST[i][k] = max(ST[i][k-1], ST[i+(1<<k-1)][k-1]);
```
> 建構 Sparse Table 的時間複雜度為 $O(n\cdot\log_2 n)$
如此能利用 $k = \lfloor\log_2(R-L) \rfloor, \max(ST(L, k), ST(R-2^k, k)$ 求得區間 $[L, R)$ 的最大值
> 注意這裡區間表示方式是**左閉右開**符號
#### 練習:
[ZEROJUDGE d539 區間 MAX](https://zerojudge.tw/ShowProblem?problemid=d539)
[TIOJ 1603 胖胖殺蚯事件](https://tioj.ck.tp.edu.tw/problems/1603)
---
# 線段樹 (Range/Interval/Segment Tree)
> 線段樹是各種區間問題的絕招,相較其他延伸應用 本節只是冰山一角
> 建議先熟悉[第三週的分治法](https://hackmd.io/@nckuacm/r1ZEy4ar8)以及[第五週的合併排序法](https://hackmd.io/@nckuacm/BJqhqDnL8)再閱讀本節
先考慮一個非常眼熟(?)的問題
:::info
給定數列 $a$,有 $Q$ 個操作包括:**更新**數列上的值或是**查詢**一個解
:::
針對更新哪些值,以及查詢哪些解,精確的分類可分為:
- 單點更新/單點查詢
- 單點更新/區間查詢
- 區間更新/單點查詢
- 區間更新/區間查詢
而所謂要求的**解**(答案)可能有:
- 極大/小值
- 總和 (或其他運算結果)
線段樹,顧名思義就是以保存很多區間解的一顆樹,例如數列長度 $5$:
```graphviz
digraph {
1 -> 2;
1 -> 3;
2 -> 4;
2 -> 5;
3 -> 6;
3 -> 7;
7 -> 8;
7 -> 9;
1[label="[1, 6)"]
2[label="[1, 3)"]
3[label="[3, 6)"]
4[label="[1, 2)"]
5[label="[2, 3)"]
6[label="[3, 4)"]
7[label="[4, 6)"]
8[label="[4, 5)"]
9[label="[5, 6)"]
}
```
(注意這裡區間表示方式是**左閉右開**符號)
## 單點更新/區間查詢
先明確的以簡單的問題設計簡單的線段樹
:::info
給定長度 $N$ 的數列 $a$,有 $Q$ 個操作包括:
- 將數列上的**一個數加上** $d_i$
- 查詢一個**區間的總和**
:::
例如 $a = (1, 15, 3, 7, 4)$,$Q = 3$ 筆詢問為:
將 $a_2$ 加上 $5$ 則 $a = (1, 20, 3, 7, 4)$
將 $a_5$ 加上 $1$ 則 $a = (1, 20, 3, 7, 5)$
詢問區間 $[2, 5)$ 的值,則區間總和為 $30$
### Node
線段樹的節點保存該區間的解,以及左右子節點的位置
```cpp
struct node {
int sum;
node *lc, *rc; // left child, right child
node() { sum = 0; lc = rc = NULL; }
};
```
### Build
根據給定數列把初始線段樹造出來
```cpp
node* build(int L, int R) {
node* u = new node();
if(R-L == 1) { // 區間中只剩一個數,也就是葉節點
u->sum = a[L];
} else {
int M = (L+R) / 2;
u->lc = build(L, M); // 左子樹
u->rc = build(M, R); // 右子樹
u->sum = u->lc->sum + u->rc->sum;
}
return u;
}
```
以 $a = (1, 15, 3, 7, 4)$ 為例,線段樹會長這樣
```graphviz
digraph {
1 -> 2;
1 -> 3;
2 -> 4;
2 -> 5;
3 -> 6;
3 -> 7;
7 -> 8;
7 -> 9;
1[label="[1, 6), 30"]
2[label="[1, 3), 16"]
3[label="[3, 6), 14"]
4[label="[1, 2), 1"]
5[label="[2, 3), 15"]
6[label="[3, 4), 3"]
7[label="[4, 6), 11"]
8[label="[4, 5), 7"]
9[label="[5, 6), 4"]
}
```
區間旁的數代表總和,例如區間 $[4, 6)$ 的總和為 $11$
### Update
將數列中某位置 $i$ 的數加上 $d$
```cpp
void update(node* u, int L, int R, int i, int d) {
if(i < L || R <= i) return; // 未包含欲更新位置
u->sum += d;
if(R-L == 1) return; // 葉節點
int M = (L+R) / 2;
update(u->lc, L, M, i, d);
update(u->rc, M, R, i, d);
}
```
以前個樹為例,將 $a_2$ 加上 $5$:
```graphviz
digraph {
edge[color=maroon]
1 -> 2;
edge[color=black]
1 -> 3;
2 -> 4;
edge[color=maroon]
2 -> 5;
edge[color=black]
3 -> 6;
3 -> 7;
7 -> 8;
7 -> 9;
1[label="[1, 6), 35", style="filled", fillcolor=maroon1]
2[label="[1, 3), 21", style="filled", fillcolor=maroon1]
3[label="[3, 6), 14"]
4[label="[1, 2), 1"]
5[label="[2, 3), 20", style="filled", fillcolor=maroon1]
6[label="[3, 4), 3"]
7[label="[4, 6), 11"]
8[label="[4, 5), 7"]
9[label="[5, 6), 4"]
}
```
每次只往包含 $i = 2$ 位置的區間遞迴下去,複雜度為 $O(\log_2 N)$
### Query
查詢區間 $[qL, qR)$ 的總和
```cpp
int query(node* u, int L, int R, int qL, int qR) {
if(R <= qL || qR <= L) return 0; // 不在欲查詢區間內
if(qL <= L && R <= qR) return u->sum;
int M = (L+R) / 2;
return query(u->lc, L, M, qL, qR) + query(u->rc, M, R, qL, qR);
}
```
以前個樹為例,詢問區間 $[2, 5)$ 的總和:
```graphviz
digraph {
edge[color=orange]
1 -> 2;
1 -> 3;
edge[color=black]
2 -> 4;
edge[color=orange]
2 -> 5;
3 -> 6;
3 -> 7;
7 -> 8;
edge[color=black]
7 -> 9;
1[label="[1, 6), 35"]
2[label="[1, 3), 21"]
3[label="[3, 6), 14"]
4[label="[1, 2), 1"]
5[label="[2, 3), 20", style="filled", fillcolor=yellow]
6[label="[3, 4), 3", style="filled", fillcolor=yellow]
7[label="[4, 6), 11"]
8[label="[4, 5), 7", style="filled", fillcolor=yellow]
9[label="[5, 6), 4"]
}
```
這樣的遞迴查詢,每一層最多只會往下遞迴兩個節點,所以複雜度為 $O(\log_2 N)$
> 利用反證法能證明當某層往下遞迴**超過兩個節點**會出現矛盾
#### 練習:
[HDU OJ 1166 敌兵布阵](http://acm.hdu.edu.cn/showproblem.php?pid=1166)
[CODEFORCES 339D Xenia and Bit Operations](https://codeforces.com/contest/339/problem/D)
\*[CODEFORCES 1253E Antenna Coverage](https://codeforces.com/problemset/problem/1253/E)
## 區間更新/區間查詢
變得棘手一些的問題
:::info
給定長度 $N$ 的數列 $a$,有 $Q$ 個操作包括:
- 將**一個區間**中數列上的數每個都**加上** $d_i$
- 查詢一個**區間的總和**
:::
例如 $a = (1, 15, 3, 7, 4)$,$Q = 2$ 筆詢問為:
將 $[2, 6)$ 加上 $3$ 則 $a = (1, 18, 6, 10, 7)$
詢問 $[2, 4)$ 的值,則區間總和為 $24$
可採用前面提的"[單點更新/區間查詢](#單點更新區間查詢)"線段樹,
欲更新區間內所有數,對**每數都單點更新**,但總複雜度為 $O(Q\cdot N\log_2 N)$
同理,這樣寫也可以做到**區間更新**:
```cpp
void update(node* u, int L, int R, int qL, int qR, int d) {
if(R <= qL || qR <= L) return;
if(R-L == 1) {
u->sum += d; // 更新該單點
return;
}
int M = (L+R) / 2;
update(u->lc, L, M, qL, qR, d);
update(u->rc, M, R, qL, qR, d);
u->sum = u->lc->sum + u->rc->sum;
}
```
以 $a = (1, 15, 3, 7, 4)$ 為例將 $[2, 6)$ 所有數加上 $3$:
```graphviz
digraph {
edge[color=maroon]
1 -> 2;
1 -> 3;
edge[color=black]
2 -> 4;
edge[color=maroon]
2 -> 5;
3 -> 6;
3 -> 7;
7 -> 8;
7 -> 9;
1[label="[1, 6), 42", style="filled", fillcolor=maroon1]
2[label="[1, 3), 19", style="filled", fillcolor=maroon1]
3[label="[3, 6), 23", style="filled", fillcolor=maroon1]
4[label="[1, 2), 1"]
5[label="[2, 3), 18", style="filled", fillcolor=maroon1]
6[label="[3, 4), 6", style="filled", fillcolor=maroon1]
7[label="[4, 6), 17", style="filled", fillcolor=maroon1]
8[label="[4, 5), 10", style="filled", fillcolor=maroon1]
9[label="[5, 6), 7", style="filled", fillcolor=maroon1]
}
```
可是複雜度仍然為 $O(Q\cdot N\log_2 N)$
<!--
> 這甚至比暴力的枚舉區間的數還來得差 :-1:
-->
### Lazy tag
顧名思義,就是懶
> 懶是工程師的美德
由於更新區間的總和同時也將其子節點一併更新,這麼*勤快*肯定會花不少時間
如果當前區間的數**全都得更新**,可先偷懶做個 tag 值表示下方的子孫節點還未更新上 tag,
等到**真正需要**查(遞迴)到其子孫節點,在將 tag 往下**推給子節點**,以完成更新
### Node
節點保存該區間的解及 tag,以及左右子節點的位置
```cpp
struct node {
int sum, tag;
node *lc, *rc;
node() { sum = tag = 0; lc = rc = NULL; }
void pull() { sum = lc->sum + rc->sum; }
void push(int L, int R) {
lc->tag += tag;
rc->tag += tag;
int M = (L+R) / 2;
lc->sum += tag * (M-L);
rc->sum += tag * (R-M);
tag = 0;
}
};
```
- `pull()` 將左右子區間的總和加起來
- `push()` 將該區間的 tag 給左右子區間,並完成子節點的更新
### Update
```cpp
void update(node* u, int L, int R, int qL, int qR, int d) {
if(R <= qL || qR <= L) return;
if(qL <= L && R <= qR) {
u->sum += d * (R-L);
u->tag += d;
return;
}
u->push(L, R);
int M = (L+R) / 2;
update(u->lc, L, M, qL, qR, d);
update(u->rc, M, R, qL, qR, d);
u->pull();
}
```
以 $a = (1, 15, 3, 7, 4)$ 為例將 $[2, 6)$ 所有數加上 $3$:
```graphviz
digraph {
edge[color=maroon]
1 -> 2;
1 -> 3;
edge[color=black]
2 -> 4;
edge[color=maroon]
2 -> 5;
edge[color=black]
3 -> 6;
3 -> 7;
7 -> 8;
7 -> 9;
1[label="[1, 6), 42", style="filled", fillcolor=maroon1]
2[label="[1, 3), 19", style="filled", fillcolor=maroon1]
3[label="[3, 6), 23", xlabel="3", style="filled", fillcolor=maroon1]
4[label="[1, 2), 1"]
5[label="[2, 3), 18", xlabel="3", style="filled", fillcolor=maroon1]
6[label="[3, 4), 3"]
7[label="[4, 6), 11"]
8[label="[4, 5), 7"]
9[label="[5, 6), 4"]
}
```
其中左上角的值代表 tag 值。
> 與沒有 lazy tag 的區間更新相比,走訪的區間少了許多
### Query
```cpp
int query(node* u, int L, int R, int qL, int qR) {
if(R <= qL || qR <= L) return 0;
if(qL <= L && R <= qR) return u->sum;
u->push(L, R);
int M = (L+R) / 2;
return query(u->lc, L, M, qL, qR) + query(u->rc, M, R, qL, qR)
}
```
以前個樹為例,詢問區間 $[2, 4)$ 的總和:
```graphviz
digraph {
edge[color=orange]
1 -> 2;
1 -> 3;
edge[color=black]
2 -> 4;
edge[color=orange]
2 -> 5;
3 -> 6;
edge[color=black]
3 -> 7;
7 -> 8;
7 -> 9;
1[label="[1, 6), 42"]
2[label="[1, 3), 19"]
3[label="[3, 6), 23"]
4[label="[1, 2), 1"]
5[label="[2, 3), 18", xlabel="3", style="filled", fillcolor=yellow]
6[label="[3, 4), 6", xlabel="3", style="filled", fillcolor=yellow]
7[label="[4, 6), 17", xlabel="3"]
8[label="[4, 5), 7"]
9[label="[5, 6), 4"]
}
```
觀察到,在查詢過程中會將 tag 往下 push 給子區間
#### 練習:
[POJ 3468 A Simple Problem with Integers](http://poj.org/problem?id=3468)
[CODEFORCES 52C Circular RMQ](https://codeforces.com/contest/52/problem/C)
---
# Binary Indexed Tree
> 又叫做 Fenwick Tree
翻譯為二元索引樹,可看作是[線段樹](#線段樹-RangeIntervalSegment-Tree)的特化版本
## 二進制
整數可由一些 $2$ 的冪 $\{2^x \mid x \ge 0\}$ 組成,且每種冪次可**只用一次**
例如 $7 = 1 + 2 + 4$,二進制表示為 $111 = 001 + 010 + 100$
每個數字可由這種方式**唯一的表示**。換句話說,兩個不同數字就得用不同方式表現
也就是說用 $(001, 010, 100)$ 表達成 $111$ 也沒問題
那麼用 $111, (001, 010)$ 表達同一件事也可以
## $\text{lowbit}(x)$
$\text{lowbit}(x)$ 指的是 $x$ 用二進制表達時最低位的 $1$ 表示的 $2$ 的冪
$x = 7$ 以二進制表達: $\text{lowbit}(111) = 001$
$x = 6$ 以二進制表達: $\text{lowbit}(110) = 010$
$x = 4$ 以二進制表達: $\text{lowbit}(100) = 100$
#### 練習:
:::warning
證明二補數系統下運算 `x&-x` 是 $x$ 的 lowbit
:::
:::warning
設 $y = x+\text{lowbit}(x)$ 證明 $y-\text{lowbit}(y)+1 \le x-\text{lowbit}(x)+1$
:::
## 單點更新/區間查詢
有了以上的基礎,可以回到正題了
:::info
給定長度 $N$ 的數列 $a$,有 $Q$ 個操作包括:
- 將數列上的**一個數加上** $d_i$
- 查詢**一個區間的總和**
:::
若只有查詢操作,透過前綴和 $S$ 做 $S(R) - S(L-1)$ 能得 $[L, R]$ 區間和
但還得考慮更新操作,所以**普通的前綴和**是不行的
> 每次單點更新就得重新計算前綴和,複雜度會過高
根據前面基礎,用 `for(; x >= 1; x-=lowbit(x)) sum += BIT[x]` 求得 $[1, x]$ 區間和
例如 $x = 7$ 那麼 `sum` 會得到 `BIT[7] + BIT[6] + BIT[4]`
其實就是把 $[1, 7]$ 區間和表達成 $111, 001, 010$
兩者結合就是:`BIT[`$111$`] + BIT[`$111-001$`] + BIT[`$111-001-010$`]`
而其中 `BIT[x]` 就能構造為 $[x-\text{lowbit}(x)+1, x]$ 的總和 $\sum\limits_{i=x-\text{lowbit}(x)+1}^x a_i$
例如 $[1, 7]$ 的總和就是 $\sum\limits_{i=1}^4 a_i + \sum\limits_{i=5}^6 a_i + \sum\limits_{i=7}^7 a_i = \sum\limits_{i=1}^7 a_i$
`BIT` 這種奇特區間定義能表示成一棵樹,例如 $[1, 8]$ 的樹:
![](https://i.imgur.com/OnS9P5a.png)
(觀察可知,這棵樹是把線段樹的右子樹通通移除的結果)
而若要更新數列的數,也要把包含此數的區間一併更新
例如更新位於位置 $x$ 的數,則 $[x-\text{lowbit}(x)+1, x]$ 要更新
$y = x+\text{lowbit}(x), [y-\text{lowbit}(y)+1, y]$ 也會包含 $x$ 位置,也得更新,依此類推
### Update
單點更新
```cpp
void update(int p, int d)
{ for(; p <= N; p+=p&-p) BIT[p] += d; }
```
複雜度為 $O(\log N)$,由於每次 lowbit 會至少進 1 位
### Query
求前綴和
```cpp
int pre(int p) {
int sum = 0;
for(; p >= 1; p-=p&-p) sum += BIT[p];
return sum;
}
```
複雜度為 $O(\log N)$,因為每次會去掉一個 bit
練習:
[CODEFORCES 356A Knight Tournament](https://codeforces.com/contest/356/problem/A)
[CODEFORCES 459D Pashmak and Parmida's problem](https://codeforces.com/contest/459/problem/D)
[CODEFORCES 1042D Petya and Array](http://codeforces.com/contest/1042/problem/D)
[TIOJ 1175 Longest Increasing Subsequence](https://tioj.ck.tp.edu.tw/problems/1175)
[TIOJ 1080 A.逆序數對](https://tioj.ck.tp.edu.tw/problems/1080)
## 區間更新/單點查詢
:::info
給定長度 $N$ 的數列 $a$,有 $Q$ 個操作包括:
- 將**一個區間**中數列上的數每個都**加上** $d_i$
- 查詢**一個位置**的值
:::
應用**差分**(Difference)技巧令 $b_i = a_i - a_{i-1}$ 且 $b_1 = a_1$
且透過 $b$ 建立 Binary Indexed Tree
### Update
則若是將所有數 $a_i$ 加上 $d$,其中 $i \in [L, R]$
那麼 $b_{R+1}$ 與 $b_L$ 會變成:
$$
\begin{cases}
a_{R+1} - (a_R+d) &= b_{R+1} - d \\
(a_{L}+d) - a_{L-1} &= b_L + d
\end{cases}
$$
也就是說 $b_{R+1}$ 增加了 $-d$ 以及 $b_L$ 增加 $d$
而其餘的 $b_i \mid i \in [L+1, R]$ 都未增加任何值,因為差分定義讓 $d$ *相消*了
則利用兩次**單點更新**
```cpp
void update(int p, int d)
{ for(; p <= N; p+=p&-p) BIT[p] += d; }
```
做 `update(L, d)` 及 `update(R+1, -d)` 就能完成**區間更新**了
### Query
根據差分定義推得 $a_i = \sum\limits_{j=1}^i b_j$,所以可利用**區間查詢**
```cpp
int pre(int p) {
int sum = 0;
for(; p >= 1; p-=p&-p) sum += BIT[p];
return sum;
}
```
做 `pre(i)` 能**單點查詢** $a_i$ 的值
## 區間更新/區間查詢
:::info
給定長度 $N$ 的數列 $a$,有 $Q$ 個操作包括:
- 將**一個區間**中數列上的數每個都**加上** $d_i$
- 查詢一個**區間的總和**
:::
根據差分定義有 $a_i = \sum\limits_{j=1}^i b_j$
能推得 $a_1 + a_2 = b_1 + (b_1 + b_2) = 2\cdot b_1 + b_2$
能推得 $a_1 + a_2 + a_3 = 2\cdot b_1 + b_2 + (b_1 + b_2 + b_3) = 3\cdot b_1 + 2\cdot b_2 + b_3$
依此類推得 $\sum\limits_{i=1}^x a_i = \sum\limits_{i=1}^x (x-i+1)\cdot b_i = (x+1)\cdot \sum\limits_{i=1}^x b_i - \sum\limits_{i=1}^x i \cdot b_i$
於是,再**多維護**一個以數列 $\{i \cdot b_i\}_{i=1}^N$ 建立的 Binary Indexed Tree 就能求前綴和了