## 基礎線段樹數據結構
(此文靈感來源於多位神犇的博客,有雷同很正常,同時向這些神犇致謝)函數
前置技能:二叉樹,遞歸,基礎分治學習
**線段樹**,一種神奇的數據結構(也是我學習的第一種進階數據結構)優化
長這樣:
ui
(同一行的各個線段**等長**~~我毫不會說是由於我圖畫的很差才說這句話~~)cdn
看起來好厲害的樣子遞歸
那麼它有什麼用??博客
它支持$O(logn)$的查詢和修改(min,max,sum,etc..) 基礎
而暴力查詢修改成$O(n)$(即便用前綴和將查詢降至$O(1)$,修改仍爲$O(n)$)變量
它的原理(爲何它比暴力優這麼多)??
暴力在查詢/修改時訪問每個點
而線段樹直接修改區間,天然很快
好比修改3——5,總區間爲1——8時

圖中數字爲左右端點編號
$\times$表示拋棄(對於與要處理的區間無關的小區間直接拋棄)
$\sqrt{} $表示處理該小區間(對於徹底屬於要處理區間的小區間直接處理)
因爲最多有$logn$層,因此單次操做複雜度$O(logn)$(帶點常數)
哦對了,線段樹的空間複雜度是$O(n)$,不是$O(nlogn)$,注意到最下面一層的節點個數就是元素個數
接下來是悲慘的寫代碼時間:(以區間和爲例)
前置代碼:一個結構體
```cpp
typedef long long ll;
struct tree
{
ll sum;//維護的值,也能夠是max,min之類(結構體的好處是成員變量名與庫函數重名時不會衝突)
ll lazy;//後面會講
}t[maxn<<1|1];//要開兩倍空間(maxn<1|1等價於maxn*2+1但更快,後面也有相似用法)
```
創建線段樹:$O(n)$
```cpp
void tain_up(ll num);
void build(ll l,ll r,ll num)
{
if(l==r)
{
scanf("%lld",&t[num].sum);
return;
}
ll mid=(l+r)>>1;//等價於mid=(l+r)/2,但更快
build(l,mid,num<<1);
build(mid+1,r,num<<1|1);
tain_up(num);
}
void tain_up(ll num)
{
t[num].sum=t[num<<1].sum+t[num<<1|1].sum;
}
```
如今講解一下:
採用遞歸,若是`l==r`,該節點就是葉子節點,直接輸入值
不然,對左右孩子遞歸處理;
tain_up做用是處理後將此節點區間和改成兩個子區間和的和
還有一種寫法,再開O(n)空間;
```cpp
for(ll i=1;i<=n;++i)scanf("%lld",&a[i]);//放主函數裏
void build(ll l,ll r,ll num)
{
if(l==r)
{
t[num]=a[l];
return;
}
ll mid=(l+r)>>1;
build(l,mid,num<<1);
build(mid+1,r,num<<1|1);
tain_up(num);
}
```
喜歡哪一種用哪一種
區間修改:$O(logn)$
```cpp
void tain_down(ll l,ll r,ll num)
{
if(!t[num].lazy)return;
ll mid=(l+r)>>1;
t[num<<1].sum+=t[num].lazy*(mid-l+1);
t[num<<1|1].sum+=t[num].lazy*(r-mid);//r-mid是r-(mid+1)+1的化簡
t[num<<1].lazy+=t[num].lazy;
t[num<<1|1].lazy+=t[num].lazy;
t[num].lazy=0;
}
scanf("%lld%lld%lld",&ql,&qr,&k);//將[ql,qr]加上k,也在主函數中
void mpart(ll l,ll r,ll num)
{
if(ql<=l&&r<=qr)//當前區間徹底屬於要求的區間直接處理
{
t[num].sum+=k*(r-l+1);
t[num].lazy+=k;
return;
}
if(ql>r||qr<l)return;//與要求的區間徹底無關直接結束
ll mid=(l+r)>>1;
tain_down(l,r,num);
mpart(l,mid,num<<1);
mpart(mid+1,r,num<<1|1);
tain_up(num);
}
```
講解:兩個特判不用說
重點在tain_down和lazy:
首先仍是要搞清楚爲何線段樹快?由於它直接修改被徹底包含的區間
但咱們在直接修改後若是要直接詢問這個區間的子區間,就會獲得錯誤的值
舉個栗子:
一開始是這樣的(黑色是左右端點,紅色是維護的值)

對[1,4]區間加4(區間中每一個點加4)
會變成

顯然[1,2]和[3,4]兩個子區間出問題了
若是咱們再詢問[1,2]的和,就會輸出5而不是正確答案13
因此咱們在處理一個區間前,要把它的值改爲對的,才能保證答案的正確性
也就是在直接處理這個區間後,爲這個區間打上標記(其子區間**每一個值**誤差lazy)
下次再詢問這個區間時,若是要繼續處理它的子區間,那麼就先把它的子區間改對,即
```cpp
if(!t[num].lazy)return;//優化,lazy爲0表示子區間沒有問題
ll mid=(l+r)>>1;
t[num<<1].sum+=t[num].lazy*(mid-l+1);//注意lazy是每一個值,不要忘了*長度
t[num<<1|1].sum+=t[num].lazy*(r-mid);//r-mid是r-(mid+1)+1的化簡
t[num<<1].lazy+=t[num].lazy;//子區間修改後,子區間的子區間也仍是有誤差的
t[num<<1|1].lazy+=t[num].lazy;
t[num].lazy=0;//別忘了清0
```
區間查詢:$O(logn)$(其實與修改相似)
```cpp
scanf("%lld%lld",&ql,&qr);//詢問[l,r]區間和
ans=0;
void qpart(ll l,ll r,ll num)
{
if(ql<=l&&r<=qr)
{
ans+=t[num].sum;
return;
}
if(l>qr||qr<l)return;
ll mid=(l+r)>>1;
tain_down(l,r,num);
qpart(l,mid,num<<1);
qpart(mid+1,r,num<<1|1);
tain_up(num);//據說這裏去掉也能夠??
}
printf("%lld\n",ans);
```
單點修改q+k只要
```cpp
scanf("%lld%lld",&q,&k);
ql=qr=q;
mpart(1,n,1);//查詢與修改都是這樣(1,n,1)使用
```
還有不懂能夠評論或私信我