線段樹從零開始

線段樹從零開始

一:爲什麼需要線段樹?
題目一:
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)定義:

[cpp] view plain copy 
  
 
  1. #define maxn 100007  //元素總個數
  2.   
  3. int Sum[maxn<<2];//Sum求和,開四倍空間

  4. int A[maxn],n;//存原數組下標[1,n]

(1)建樹:

[cpp] view plain copy 
  
 
  1. //PushUp函數更新節點信息,這裏是求和

  2. void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}  

  3. //Build函數建立線段樹

  4. void Build(int l,int r,int rt){ //[l,r]表示當前節點區間,rt表示當前

  5. 節點的實際存儲位置
  6.  
  7.     if(l==r) {//若到達葉節點 

  8.         Sum[rt]=A[l];//存儲A數組的值

  9.         return;  

  10.     }  

  11.     int m=(l+r)>>1;  

  12.    //左右遞歸

  13.     Build(l,m,rt<<1);  

  14.     Build(m+1,r,rt<<1|1);  

  15.     //更新信息

  16.     PushUp(rt);  

  17. }  


(2)點修改:

假設A[L]+=C:
[cpp] view plain copy 
  
 
  1. void Update(int L,int C,int l,int r,int rt){//[l,r]表示當前區間,rt

  2. 是當前節點編號//l,r表示當前節點區間,rt表示當前節點編號  
  3.     
  4. if(l==r){//到達葉節點,修改葉節點的值

  5.         Sum[rt]+=C;  

  6.         return;  

  7.     }  

  8.     int m=(l+r)>>1;  

  9.    //根據條件判斷往左子樹調用還是往右

  10.     if(L <= m) Update(L,C,l,m,rt<<1);
  11.   
  12.     else       Update(L,C,m+1,r,rt<<1|1);
  13.   
  14.     PushUp(rt);//子節點的信息更新了,所以本節點也要更新信息

  15. }   

點修改其實可以寫的更簡單,只需要把一路經過的Sum都+=C就行了,不過上面的代碼更加規範,在題目更加複雜的時候,按照格式寫更不容易錯。


(3)區間查詢(本題爲求和):

詢問A[L..R]的和
注意到,整個函數的遞歸過程中,L,R是不變的。
首先如果當前區間[l,r]在[L,R]內部,就直接累加答案
如果左子區間與[L,R]有重疊,就遞歸左子樹,右子樹同理。
[cpp] view plain copy 
  
 
  1. int Query(int L,int R,int l,int r,int rt){//[L,R]表示操作區間,

  2. [l,r]表示當前區間,rt:當前節點編號

  3.     if(L <= l && r <= R){  

  4.        //在區間內直接返回

  5.         return Sum[rt];  

  6.     }  

  7.     int m=(l+r)>>1;  

  8.     //左子區間:[l,m] 右子區間:[m+1,r]  求和區間:[L,R]

  9.    //累加答案

  10.     int ANS=0;  

  11.     if(L <= m) ANS+=Query(L,R,l,m,rt<<1);//左子區間與[L,R]有重疊,遞

  12.     
  13.     if(R >  m) ANS+=Query(L,R,m+1,r,rt<<1|1); //右子區間與[L,R]有重疊,遞歸

  14.     return ANS; 
  15.  
  16. }   




最後:

線段樹還有更多的用法,比如區間修改,掃描線,非遞歸寫法等,詳見這篇文章: 線段樹詳解

人一我百!人十我萬!永不放棄~~~懷着自信的心,去追逐夢想。

發佈了74 篇原創文章 · 獲贊 11 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章