線段樹從零開始
原文連結:https://blog.csdn.net/zearot/article/details/52280189
一:為什麼需要線段樹?
題目一: 10000個正整數,編號1到10000,用A[1],A[2],A[10000]表示。 修改:無 統計:1.編號從L到R的所有數之和為多少? 其中1<= L <= R <= 10000. |
方法一:對於統計L,R ,需要求下標從L到R的所有數的和,從L到R的所有下標記做[L..R],問題就是對A[L..R]進行求和。
這樣求和,對於每個詢問,需要將(R-L+1)個數相加。
方法二:更快的方法是求字首和,令 S[0]=0, S[k]=A[1..k] ,那麼,A[L..R]的和就等於S[R]-S[L-1],
這樣,對於每個詢問,就只需要做一次減法,大大提高效率。
題目二: 10000個正整數,編號從1到10000,用A[1],A[2],A[10000]表示。 修改:1.將第L個數增加C (1 <= L <= 10000) 統計:1.編號從L到R的所有數之和為多少? 其中1<= L <= R <= 10000. |
再使用方法二的話,假如A[L]+=C之後,S[L],S[L+1],,S[R]都需要增加C,全部都要修改,見下表。
方法一 | 方法二 | |
A[L]+=C | 修改1個元素 | 修改R-L+1個元素 |
求和A[L..R] | 計算R-L+1個元素之和 | 計算兩個元素之差 |
從上表可以看出,方法一修改快,求和慢。 方法二求和快,修改慢。
那有沒有一種結構,修改和求和都比較快呢?答案當然是線段樹。
二:線段樹的點修改
上面的問題二就是典型的線段樹點修改。
線段樹先將區間[1..10000]分成不超過4*10000個子區間,對於每個子區間,記錄一段連續數字的和。
之後,任意給定區間[L,R],線段樹在上述子區間中選擇約2*log2(R-L+1)個拼成區間[L,R]。
如果A[L]+=C ,線段樹的子區間中,約有log2(10000)個包含了L,所以需要修改log2(10000)個。
於是,使用線段樹的話,
A[L]+=C 需要修改log2(10000) 個元素
求和A[L...R]需要修改2*log2(R-L+1) <= 2 * log2(10000) 個元素。
log2(10000) < 14 所以相對來說線段樹的修改和求和都比較快。
問題一:開始的子區間是怎麼分的?
首先是講原始子區間的分解,假定給定區間[L,R],只要L < R ,線段樹就會把它繼續分裂成兩個區間。
首先計算 M = (L+R)/2,左子區間為[L,M],右子區間為[M+1,R],然後如果子區間不滿足條件就遞迴分解。
以區間[1..13]的分解為例,分解結果見下圖:
問題二:給定區間【L,R】,如何分解成上述給定的區間?
對於給定區間[2,12]要如何分解成上述區間呢?
分解方法一:自下而上合併——利於理解
先考慮樹的最下層,將所有在區間[2,12]內的點選中,然後,若相鄰的點的直接父節點是同一個,那麼就用這個父節點代替這兩個節點(父節點在上一層)。這樣操作之後,本層最多剩下兩個節點。若最左側被選中的節點是它父節點的右子樹,那麼這個節點會被剩下。若最右側被選中的節點是它的父節點的左子樹,那麼這個節點會被剩下。中間的所有節點都被父節點取代。對最下層處理完之後,考慮它的上一層,繼續進行同樣的處理。
下圖為n=13的線段樹,區間[2,12],按照上面的敘述進行操作的過程圖:
由圖可以看出:在n=13的線段樹中,[2,12]=[2] + [3,4] + [5,7] + [8,10] + [11,12] 。
分解方法二:自上而下分解——利於計算
首先對於區間[1,13],計算(1+13)/2 = 7,於是將區間[2,12]“切割”成了[2,7]和[8,12]。
其中[2,7]處於節點[1,7]的位置,[2,7] < [1,7] 所以繼續分解,計算(1+7)/2 = 4, 於是將[2,7] 切割成[2,4]和[5,7]。
[5,7]處於節點[5,7]的位置,所以不用繼續分解,[2,4]處於區間[1,4]的位置,所以繼續分解成[2]和[3,4]。
最後【2】 < 【1,2】,所以計算(1+2)/2=1 ,將【2】用1切割,左側為空,右側為【2】
當然程式是遞迴計算的,不是一層一層計算的,上圖只表示計算方法,不代表計算順序。
問題三:如何進行區間統計?
假設這13個數為1,2,3,4,1,2,3,4,1,2,3,4,1. 在區間之後標上該區間的數字之和:
如果要計算[2,12]的和,按照之前的演算法:
[2,12]=[2] + [3,4] + [5,7] + [8,10] + [11,12]
29 = 2 + 7 + 6 + 7 + 7
計算5個數的和就可以算出[2,12]的值。
問題四:如何進行點修改?
假設把A[6]+=7 ,看看哪些區間需要修改?[6],[5,6],[5,7],[1,7],[1,13]這些區間全部都需要+7.其餘所有區間都不用動。
於是,這顆線段樹中,點修改最多修改5個線段樹元素(每層一個)。
下圖中,修改後的元素用藍色表示。
問題五:儲存結構是怎樣的?
線段樹是一種二叉樹,當然可以像一般的樹那樣寫成結構體,指標什麼的。
但是它的優點是,它也可以用陣列來實現樹形結構,可以大大簡化程式碼。
陣列形式適合在程式設計競賽中使用,在已經知道線段樹的最大規模的情況下,直接開足夠空間的陣列,然後在上面建立線段樹。
簡單的記法: 足夠的空間 = 陣列大小n的四倍。
實際上足夠的空間 = (n向上擴充到最近的2的某個次方)的兩倍。
舉例子:假設陣列長度為5,就需要5先擴充成8,8*2=16.線段樹需要16個元素。如果陣列元素為8,那麼也需要16個元素。
所以線段樹需要的空間是n的兩倍到四倍之間的某個數,一般就開4*n的空間就好,如果空間不夠,可以自己算好最大值來省點空間。
怎麼用陣列來表示一顆二叉樹呢?假設某個節點的編號為v,那麼它的左子節點編號為2*v,右子節點編號為2*v+1。
然後規定根節點為1.這樣一顆二叉樹就構造完成了。通常2*v在程式碼中寫成 v<<1 。 2*v+1寫成 v<<1|1 。
問題六:程式碼中如何實現?
(0)定義:
#define maxn 100007 //元素總個數
int Sum[maxn<<2];//Sum求和,開四倍空間
int A[maxn],n;//存原陣列下標[1,n]
(1)建樹:
//PushUp函式更新節點資訊,這裡是求和
void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}
//Build函式建立線段樹
void Build(int l,int r,int rt){ //[l,r]表示當前節點區間,rt表示當前節點的實際儲存位置
if(l==r) {//若到達葉節點
Sum[rt]=A[l];//儲存A陣列的值
return;
}
int m=(l+r)>>1;
//左右遞迴
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
//更新資訊
PushUp(rt);
}
(2)點修改:
假設A[L]+=C:
void Update(int L,int C,int l,int r,int rt){//[l,r]表示當前區間,rt是當前節點編號//l,r表示當前節點區間,rt表示當前節點編號
if(l==r){//到達葉節點,修改葉節點的值
Sum[rt]+=C;
return;
}
int m=(l+r)>>1;
//根據條件判斷往左子樹呼叫還是往右
if(L <= m) Update(L,C,l,m,rt<<1);
else Update(L,C,m+1,r,rt<<1|1);
PushUp(rt);//子節點的資訊更新了,所以本節點也要更新資訊
}
點修改其實可以寫的更簡單,只需要把一路經過的Sum都+=C就行了,不過上面的程式碼更加規範,在題目更加複雜的時候,按照格式寫更不容易錯。
(3)區間查詢(本題為求和):
詢問A[L..R]的和
注意到,整個函式的遞迴過程中,L,R是不變的。
首先如果當前區間[l,r]在[L,R]內部,就直接累加答案
如果左子區間與[L,R]有重疊,就遞迴左子樹,右子樹同理。
int Query(int L,int R,int l,int r,int rt){//[L,R]表示操作區間,[l,r]表示當前區間,rt:當前節點編號
if(L <= l && r <= R){
//在區間內直接返回
return Sum[rt];
}
int m=(l+r)>>1;
//左子區間:[l,m] 右子區間:[m+1,r] 求和區間:[L,R]
//累加答案
int ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);//左子區間與[L,R]有重疊,遞迴
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1); //右子區間與[L,R]有重疊,遞迴
return ANS;
}
相關文章
- 從零開始實現線上直播
- 從零開始
- 不怕從零開始,只怕從未開始!
- 線段樹 - 多組圖帶你從頭到尾徹底理解線段樹
- VUE2.0從零開始 學習路線Vue
- 從零開始學PythonPython
- flutter 從零開始-1Flutter
- 樹莓派從零開始搭建Samba檔案伺服器樹莓派Samba伺服器
- 動態開點線段樹
- 線段樹
- 線~段~樹
- 從零開始的簡單光線追蹤示例
- 線段樹模版:從入門到入墳
- 從零開始認識 SparkSpark
- 從零開始學習laravelLaravel
- 幾何庫從零開始
- 從零開始學習KafkaKafka
- 【ROS】從零開始學ROSROS
- 從零開始--webpack 4 配置Web
- 從零開始學 Spring BootSpring Boot
- 從零開始學正則
- 從零開始-打造一個JavaScript完整線上教程文件JavaScript
- 大資料學習路線(自己制定,從零開始)大資料
- 01 線段樹
- 線段樹--RMQMQ
- 【模版】線段樹
- 線段樹模板
- 線段樹 hate it
- 從零開始開發一個 WebpackWeb
- 動態開點線段樹說明
- 從零開始的Flutter之旅: NavigatorFlutter
- 從零開始認識堆排序排序
- 從零開始的Flutter之旅: ProviderFlutterIDE
- 從零開始的Flutter之旅: InheritedWidgetFlutter
- 從零開始的Flutter之旅: StatelessWidgetFlutter
- VUE從零開始環境搭建Vue
- 從零開始的Flutter之旅: MethodChannelFlutter
- 從零開始的 Flutter 動畫Flutter動畫