# 線段樹
> [color=blue]於競賽而言強大的工具。
## 用法 & 概念
線段樹除了可以用來快速解區間和問題,還可以用來執行許多與區間有關的操作。大致上用以下方式建構區間
```graphviz
digraph SEG{
nodesep=0.7 // increases the separation between nodes
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=dashed] //All the lines look like this
"1~8"->{"1~4" "5~8"}
"1~4"->{"1~2" "3~4"}
"1~2"->{"1" "2"}
"3~4"->{"3" "4"}
"5~8"->{"5~6" "7~8"}
"5~6"->{"5" "6"}
"7~8"->{"7" "8"}
}
```
每個區間視情況放不同的數值,例如:最大/小,或是區間總和等。
接著,每個區間就都可以分為$O(log(n))$個區間,例如`2~7`可以分為`2, 3~4, 5~6, 7`
```graphviz
digraph SEG{
nodesep=0.7 // increases the separation between nodes
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=dashed] //All the lines look like this
"1~8"->{"1~4" "5~8"}
"1~4"->{"1~2" "3~4"[color=DarkGreen]}
"1~2"->{"1" "2"[color=DarkGreen]}
"3~4"->{"3" "4"}
"5~8"->{"5~6"[color=DarkGreen] "7~8"}
"5~6"->{"5" "6"}
"7~8"->{"7"[color=DarkGreen] "8"}
}
```
查詢時皆以最大區間為出發點,例如上圖就會是從`1~8`這個區間開始,如果要查詢的區間是`2~7`。因為`1~8`這個區間並沒有完全包含`2~7`,因此需要往下遞迴,分成`1~4`和`5~8`再次查詢。
接著,因為`1~4`和`5~8`仍然沒有完全被`2~7`包含,因此要再次遞迴,這次是分解成`1~2`,`3~4`,`5~6`以及`7~8`。
這次`3~4`和`5~6`都有被完全包含,因此可以直接回傳這個區間的值。而`1~2`和`7~8`還是沒有。所以這兩個區間還要再次向下查詢。
查詢時最重要的是,若區間完全被包含就直接回傳,若完全沒被包含就不往那邊搜尋,否則再將區間分成兩塊向下遞迴。
## 實作
可以發現他是一顆二元樹,於是我們有兩種做法:指標型與陣列型。以下實作以區間總和為範例。
### 指標型
請詳讀程式碼,我盡可能詳細註解
```cpp=
#include<bits/stdc++.h>
using namespace std;
int n;
struct node{
//value
int val;
//左右子樹的指標
node *rch,*lch;
//建構式
node(){
val=0;
rch=lch=nullptr;
}
//帶有初始值的建構式
node(int v){
val=v;
rch=lch=nullptr;
}
//當左右子樹改變時,應重新計算父節點的值
void pull(){
//歸零
val=0;
//必須先有左右子樹才能拜訪
//if(l) 相當於 if(l!=nullptr)
if(lch) val+=lch->val;
if(rch) val+=rch->val;
}
//單點修改
void modify(int p,int v,int lb=1,int rb=n){
//終止條件:區間長度為1
if(lb==rb){
val=v;
return;
}
//若左右子樹沒有結點則開一個新的
if(!lch) lch=new node();
if(!rch) rch=new node();
//設mid為區間中點,均分為左右區間
//>>1相當於/2,但快很多
int mid=lb+rb>>1;
//左邊走左,右邊走右
if(p<=mid) lch->modify(p,v, lb ,mid);
if(mid<p) rch->modify(p,v,mid+1,rb);
//還記得子樹修改完要做什麼?
pull();
}
int query(int l,int r,int lb=1,int rb=n){
//終止條件:所在區間位於欲查詢區間之中
if(l<=lb && rb<=r){
return val;
}
//同modify
int mid=lb+rb>>1;
//左右遞迴求解
int ret=0;
//若左右區間不在要查詢的區間或是沒有左右子樹則不遞迴
if(lch && l<=mid) ret+=lch->query(l,r, lb ,mid);
if(rch && mid<r) ret+=rch->query(l,r,mid+1,rb);
//為求保險,以及供以後懶人標記使用
pull();
return ret;
}
};
node *rt=new node();//root
```
### 陣列型
> [color=blue] **設根節點idx為1,在完滿二元樹中,左子樹就會是$idx \times 2$,右子樹就是$idx \times 2+1$。**
```cpp=
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N];
//陣列seg的大小建議是N*4,否則你要先知道大於N的最小2^k值
// 131072 -> 262144 所以其實開 262200 就可以了
int seg[N*4];
int n,MXN=1;
//不同於指標型的是:陣列行要預建構
void build(int lb=1,int rb=MXN,int idx=1){
//終止條件:區間長度為1
if(lb==rb) return;
//設mid為區間中點,均分為左右區間
//>>1相當於/2,但稍快
int mid=lb+rb>>1;
//idx*2 為左子樹
//idx*2+1 為右子樹
build(lb,mid,idx*2);
build(mid+1,rb,idx*2+1);
seg[idx]=seg[idx*2]+seg[idx*2+1];
}
void init(){
//為了讓他長度為2^k
while(MXN<n) MXN<<=1;
//歸零
for(int i=MXN+1;i<MXN*2;i++) seg[i]=0;
//以陣列的內容對其初始化
for(int i=1;i<=n;++i) seg[MXN+i-1]=a[i];
//將所有節點都建構好
build();
}
void modify(int x,int k){
//此部分也與指標型不同,陣列可以直接依據座標修改
x=x+MXN-1;
seg[x]=k;
//向上更新節點
while(x>1){
x>>=1;
seg[x]=seg[x*2]+seg[x*2+1];
}
}
int query(int l,int r,int lb=1,int rb=MXN,int idx=1){
//終止條件:所在區間位於欲查詢區間之中
if(l<=lb && rb<=r) return seg[idx];
//設mid為區間中點,均分為左右區間
//>>1相當於/2,但稍快
int mid=lb+rb>>1;
//左右遞迴求解
int ret=0;
//若左右區間不在要查詢的區間或是沒有左右子樹則不遞迴
if(l<=mid) ret+=query(l,r, lb ,mid,idx*2);
if(r>=mid+1) ret+=query(l,r,mid+1,rb,idx*2+1);
return ret;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
//一定要記得init()
init();
}
```
## 懶人標記
以上的實作方法都只能用於單點修改,而若需要區間加值的話則會很費時,因此有人想到一個方法:若要修改的區間剛好完全包含線段樹上的某個區間的話,就可以先在上面打一個標記。等到需要動到他下面的子區間再向下放。這樣的話區間加值也可以從 $O(nlog(n))$ 進步到 $O(log(n))$ 。
### 實作
#### 指標型
請詳讀程式碼,我盡可能詳細註解
```cpp=
#include<bits/stdc++.h>
using namespace std;
int n;
struct node{
//value tag
int val,tag;
//左右子樹的指標
node *rch,*lch;
//建構式
node(){
val=tag=0;
rch=lch=nullptr;
}
//帶有初始值的建構式
node(int v){
val=v, tag=0;
rch=lch=nullptr;
}
//多了一個push(),將標記下放
void push(int l,int r){
//>>運算會在+-之後
int len=r-l+1>>1;
if(lch){
lch->val+=tag*len;
lch->tag+=tag;
}
if(rch){
rch->val+=tag*len;
rch->tag+=tag;
}
tag=0;
}
//當左右子樹改變時,應重新計算父節點的值
void pull(){
//歸零
val=0;
//必須先有左右子樹才能拜訪
//if(l) 相當於 if(l!=nullptr)
if(lch) val+=lch->val;
if(rch) val+=rch->val;
}
//單點修改
void modify(int p,int v,int lb=1,int rb=n){
//終止條件:區間長度為1
if(lb==rb){
val=v;
return;
}
//若左右子樹沒有結點則開一個新的
if(!lch) lch=new node();
if(!rch) rch=new node();
//在遞迴之前先下放標記
push(lb,rb);
//設mid為區間中點,均分為左右區間
//>>1相當於/2,但快很多
int mid=lb+rb>>1;
//左邊走左,右邊走右
if(p<=mid) lch->modify(p,v, lb ,mid);
if(mid<p) rch->modify(p,v,mid+1,rb);
//還記得子樹修改完要做什麼?
pull();
}
int query(int l,int r,int lb=1,int rb=n){
//終止條件:所在區間位於欲查詢區間之中
if(l<=lb && rb<=r){
return val;
}
//在遞迴之前先下放標記
push(lb,rb);
//同modify
int mid=lb+rb>>1;
//左右遞迴求解
int ret=0;
//若左右區間不在要查詢的區間或是沒有左右子樹則不遞迴
if(lch && l<=mid) ret+=lch->query(l,r, lb ,mid);
if(rch && mid<r) ret+=rch->query(l,r,mid+1,rb);
pull();
return ret;
}
//多了一個函式,區間加值
void add(int l,int r,int v,int lb=1,int rb=n){
//終止條件:所在區間位於欲查詢區間之中
if(l<=lb && rb<=r){
//由於區間為閉區間[lb,rb]因此要加1
val+=v*(rb-lb+1);
tag+=v;
return;
}
//在遞迴之前先下放標記
push(lb,rb);
//同modify
int mid=lb+rb>>1;
//左右遞迴求解
int ret=0;
//若左右區間不在要查詢的區間或是沒有左右子樹則不遞迴
if(lch && l<=mid) lch->add(l,r,v, lb ,mid);
if(rch && mid<r) rch->add(l,r,v,mid+1,rb);
pull();
}
};
node rt;//root
```
## 習題
### [Segment Tree 練習(ZJe409)](https://zerojudge.tw/ShowProblem?problemid=e409)
#### 題目概述 :
將 $A[x]$的值更新為 $y$
要查詢 $A[X]$~$A[Y]$ 之中最大值$maxA$及最小值$minA$的差
#### 輸入說明 :
第1列有兩個正整數 $k$ $m$ ,第2列有 $k$ 個正整數, 請讀入放至 $A[1..k]$
接著讀入第3列的 $N$ $Q$ 兩個正整數,呼叫所附的產生測資程式 `gen_dat()`
以上 $5<k \leq n, 5<m<1000$
然後就是解題,依$C,X,Y$陣列的值 執行更新或查詢
#### 輸出說明 :
若為調查,則輸出區域中的元素總和,若為修改,則不必輸出
#### 範例輸入 1 :
```
10 541
12 34 56 78 91 23 45 67 89 111
25 15
```
#### 範例輸出 1 :
```
328
68
406
0
79
251
327
489
0
```
---
### [戰術資料庫(110宜中資訊社校內賽pF)](https://zerojudge.tw/ShowProblem?problemid=g625)
#### 題目敘述 :
要求能在一個陣列中做以下操作
1. 搜尋區間和 $find sum$ $(l)$ $(r)$
2. 搜尋區間最大和最小值 $find$ $max/min (l)$ $(r)$
3. 單點加減值 $plus/minus$ $(position)$ $(k)$
#### 輸入說明 :
輸入第一行有一個數字 n, q ,下一行有 n 個數字,緊接著有 q 筆操作,可能為以下五種
1. 搜尋區間和
2. 搜尋區間最大和最小值
3. 單點加減值
相鄰數字間以空白隔開。
$n, q \leq 100000 , a[i] \leq 100000$
#### 輸出說明 :
對於每個搜尋指令做出回答
#### 範例輸入 1 :
```
7 10
1 2 3 4 5 6 7
find max 2 5
find min 1 4
minus 3 1
plus 2 4
find sum 1 7
plus 7 -3
find sum 1 3
minus 6 0
plus 1 1
find max 1 7
```
#### 範例輸出 1 :
```
5
1
31
9
6
```
> **2022/1/10**
> [name=mtmatt] [time=Mon, Jan 10, 2022 21:10 ] [color=#00eeff]