線段樹從零開始

Enjoy_process發表於2018-09-02

原文連結: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;  
}   

 

相關文章