1. 程式人生 > >線段樹 詳解

線段樹 詳解

線段樹詳解

By 巖之痕

目錄:

一:綜述

二:原理

三:遞迴實現

四:非遞迴原理

五:非遞迴實現

六:線段樹解題模型

七:掃描線

八:可持久化 (主席樹)

九:練習題

一:綜述

假設有編號從1到n的n個點,每個點都存了一些資訊,用[L,R]表示下標從L到R的這些點。

線段樹的用處就是,對編號連續的一些點進行修改或者統計操作,修改和統計的複雜度都是O(log2(n)).

線段樹的原理,就是,將[1,n]分解成若干特定的子區間(數量不超過4*n),然後,將每個區間[L,R]都分解為

少量特定的子區間,通過對這些少量子區間的修改或者統計,來實現快速對[L,R]的修改或者統計。

由此看出,用線段樹統計的東西,必須符合區間加法,否則,不可能通過分成的子區間來得到[L,R]的統計結果。

符合區間加法的例子:

數字之和——總數字之和 = 左區間數字之和 + 右區間數字之和

最大公因數(GCD)——總GCD = gcd( 左區間GCD , 右區間GCD );

最大值——總最大值=max(左區間最大值,右區間最大值)

不符合區間加法的例子:

眾數——只知道左右區間的眾數,沒法求總區間的眾數

01序列的最長連續零——只知道左右區間的最長連續零,沒法知道總的最長連續零

一個問題,只要能化成對一些連續點的修改和統計問題,基本就可以用線段樹來解決了,具體怎麼轉化在第六節會講。

由於點的資訊可以千變萬化,所以線段樹是一種非常靈活的資料結構,可以做的題的型別特別多,只要會轉化。

線段樹當然是可以維護線段資訊的,因為線段資訊也是可以轉換成用點來表達的(每個點代表一條線段)。

所以在以下對結構的討論中,都是對點的討論,線段和點的對應關係在第七節掃描線中會講。

本文二到五節是講對線段樹操作的原理和實現。

六到八節介紹了線段樹解題模型,以及一些例題。

初學者可以先看這篇文章: 線段樹從零開始

二:原理

(注:由於線段樹的每個節點代表一個區間,以下敘述中不區分節點和區間,只是根據語境需要,選擇合適的詞)

線段樹本質上是維護下標為1,2,..,n的n個按順序排列的數的資訊,所以,其實是“點樹”,是維護n的點的資訊,至於每個點的資料的含義可以有很多,

在對線段操作的線段樹中,每個點代表一條線段,在用線段樹維護數列資訊的時候,每個點代表一個數,但本質上都是每個點代表一個數。以下,在討論線段樹的時候,區間[L,R]指的是下標從L到R的這(R-L+1)個數,而不是指一條連續的線段。只是有時候這些數代表實際上一條線段的統計結果而已。

線段樹是將每個區間[L,R]分解成[L,M]和[M+1,R] (其中M=(L+R)/2 這裡的除法是整數除法,即對結果下取整)直到 L==R 為止。 

開始時是區間[1,n] ,通過遞迴來逐步分解,假設根的高度為1的話,樹的最大高度為(n>1)。

線段樹對於每個n的分解是唯一的,所以n相同的線段樹結構相同,這也是實現可持久化線段樹的基礎。

下圖展示了區間[1,13]的分解過程:

上圖中,每個區間都是一個節點,每個節點存自己對應的區間的統計資訊。

(1)線段樹的點修改:

假設要修改[5]的值,可以發現,每層只有一個節點包含[5],所以修改了[5]之後,只需要每層更新一個節點就可以線段樹每個節點的資訊都是正確的,所以修改次數的最大值為層數

複雜度O(log2(n))

(2)線段樹的區間查詢:

線段樹能快速進行區間查詢的基礎是下面的定理:

定理:n>=3時,一個[1,n]的線段樹可以將[1,n]的任意子區間[L,R]分解為不超過個子區間。

這樣,在查詢[L,R]的統計值的時候,只需要訪問不超過個節點,就可以獲得[L,R]的統計資訊,實現了O(log2(n))的區間查詢。

下面給出證明:

(2.1)先給出一個粗略的證明(結合下圖):

先考慮樹的最下層,將所有在區間[L,R]內的點選中,然後,若相鄰的點的直接父節點是同一個,那麼就用這個父節點代替這兩個節點(父節點在上一層)。這樣操作之後,本層最多剩下兩個節點。若最左側被選中的節點是它父節點的右子樹,那麼這個節點會被剩下。若最右側被選中的節點是它的父節點的左子樹,那麼這個節點會被剩下。中間的所有節點都被父節點取代。

對最下層處理完之後,考慮它的上一層,繼續進行同樣的處理,可以發現,每一層最多留下2個節點,其餘的節點升往上一層,這樣可以說明分割成的區間(節點)個數是大概是樹高的兩倍左右。

下圖為n=13的線段樹,區間[2,12],按照上面的敘述進行操作的過程圖:

由圖可以看出:在n=13的線段樹中,[2,12]=[2] + [3,4] + [5,7] + [8,10] + [11,12] 。

下圖是n=16 , L=2 , R=15 時的操作圖,此圖展示了達到最小上界的樹的結構。

(3)線段樹的區間修改:

線段樹的區間修改也是將區間分成子區間,但是要加一個標記,稱作懶惰標記。

標記的含義:

本節點的統計資訊已經根據標記更新過了,但是本節點的子節點仍需要進行更新。

即,如果要給一個區間的所有值都加上1,那麼,實際上並沒有給這個區間的所有值都加上1,而是打個標記,記下來,這個節點所包含的區間需要加1.打上標記後,要根據標記更新本節點的統計資訊,比如,如果本節點維護的是區間和,而本節點包含5個數,那麼,打上+1的標記之後,要給本節點維護的和+5。這是向下延遲修改,但是向上顯示的資訊是修改以後的資訊,所以查詢的時候可以得到正確的結果。有的標記之間會相互影響,所以比較簡單的做法是,每遞迴到一個區間,首先下推標記(若本節點有標記,就下推標記),然後再打上新的標記,這樣仍然每個區間操作的複雜度是O(log2(n))。

標記有相對標記絕對標記之分:

相對標記是將區間的所有數+a之類的操作,標記之間可以共存,跟打標記的順序無關(跟順序無關才是重點)。

所以,可以在區間修改的時候不下推標記,留到查詢的時候再下推。

      注意:如果區間修改時不下推標記,那麼PushUp函式中,必須考慮本節點的標記。

                 而如果所有操作都下推標記,那麼PushUp函式可以不考慮本節點的標記,因為本節點的標記一定已經被下推了(也就是對本節點無效了)

絕對標記是將區間的所有數變成a之類的操作,打標記的順序直接影響結果,

所以這種標記在區間修改的時候必須下推舊標記,不然會出錯。

注意,有多個標記的時候,標記下推的順序也很重要,錯誤的下推順序可能會導致錯誤。

之所以要區分兩種標記,是因為非遞迴線段樹只能維護相對標記。

因為非遞迴線段樹是自底向上直接修改分成的每個子區間,所以根本做不到在區間修改的時候下推標記。

非遞迴線段樹一般不下推標記,而是自下而上求答案的過程中,根據標記更新答案。

(4)線段樹的儲存結構:

線段樹是用陣列來模擬樹形結構,對於每一個節點R ,左子節點為 2*R (一般寫作R<<1)右子節點為 2*R+1(一般寫作R<<1|1)

然後以1為根節點,所以,整體的統計資訊是存在節點1中的。

這麼表示的原因看下圖就很明白了,左子樹的節點標號都是根節點的兩倍,右子樹的節點標號都是左子樹+1:

線段樹需要的陣列元素個數是:,一般都開4倍空間,比如: int A[n<<2];

三:遞迴實現

以下以維護數列區間和的線段樹為例,演示最基本的線段樹程式碼。

(0)定義:

  1. #define maxn 100007  //元素總個數  
  2. #define ls l,m,rt<<1  
  3. #define rs m+1,r,rt<<1|1  
  4. int Sum[maxn<<2],Add[maxn<<2];//Sum求和,Add為懶惰標記   
  5. int A[maxn],n;//存原陣列資料下標[1,n]   

(1)建樹:

  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.     if(l==r) {//若到達葉節點   
  6.         Sum[rt]=A[l];//儲存陣列值   
  7.         return;  
  8.     }  
  9.     int m=(l+r)>>1;  
  10.     //左右遞迴   
  11.     Build(l,m,rt<<1);  
  12.     Build(m+1,r,rt<<1|1);  
  13.     //更新資訊   
  14.     PushUp(rt);  
  15. }  

(2)點修改:

假設A[L]+=C:

  1. void Update(int L,int C,int l,int r,int rt){//l,r表示當前節點區間,rt表示當前節點編號  
  2.     if(l==r){//到葉節點,修改   
  3.         Sum[rt]+=C;  
  4.         return;  
  5.     }  
  6.     int m=(l+r)>>1;  
  7.     //根據條件判斷往左子樹呼叫還是往右   
  8.     if(L <= m) Update(L,C,l,m,rt<<1);  
  9.     else       Update(L,C,m+1,r,rt<<1|1);  
  10.     PushUp(rt);//子節點更新了,所以本節點也需要更新資訊   
  11. }   

(3)區間修改:

假設A[L,R]+=C

  1. void Update(int L,int R,int C,int l,int r,int rt){//L,R表示操作區間,l,r表示當前節點區間,rt表示當前節點編號   
  2.     if(L <= l && r <= R){//如果本區間完全在操作區間[L,R]以內   
  3.         Sum[rt]+=C*(r-l+1);//更新數字和,向上保持正確  
  4.         Add[rt]+=C;//增加Add標記,表示本區間的Sum正確,子區間的Sum仍需要根據Add的值來調整  
  5.         return ;   
  6.     }  
  7.     int m=(l+r)>>1;  
  8.     PushDown(rt,m-l+1,r-m);//下推標記  
  9.     //這裡判斷左右子樹跟[L,R]有無交集,有交集才遞迴   
  10.     if(L <= m) Update(L,R,C,l,m,rt<<1);  
  11.     if(R >  m) Update(L,R,C,m+1,r,rt<<1|1);   
  12.     PushUp(rt);//更新本節點資訊   
  13. }   

(4)區間查詢:

詢問A[L,R]的和

首先是下推標記的函式:

  1. void PushDown(int rt,int ln,int rn){  
  2.     //ln,rn為左子樹,右子樹的數字數量。   
  3.     if(Add[rt]){  
  4.         //下推標記   
  5.         Add[rt<<1]+=Add[rt];  
  6.         Add[rt<<1|1]+=Add[rt];  
  7.         //修改子節點的Sum使之與對應的Add相對應   
  8.         Sum[rt<<1]+=Add[rt]*ln;  
  9.         Sum[rt<<1|1]+=Add[rt]*rn;  
  10.         //清除本節點標記   
  11.         Add[rt]=0;  
  12.     }  
  13. }  

然後是區間查詢的函式:

  1. int Query(int L,int R,int l,int r,int rt){//L,R表示操作區間,l,r表示當前節點區間,rt表示當前節點編號  
  2.     if(L <= l && r <= R){  
  3.         //在區間內,直接返回   
  4.         return Sum[rt];  
  5.     }  
  6.     int m=(l+r)>>1;  
  7.     //下推標記,否則Sum可能不正確  
  8.     PushDown(rt,m-l+1,r-m);   
  9.     //累計答案  
  10.     int ANS=0;  
  11.     if(L <= m) ANS+=Query(L,R,l,m,rt<<1);  
  12.     if(R >  m) ANS+=Query(L,R,m+1,r,rt<<1|1);  
  13.     return ANS;  
  14. }   

(5)函式呼叫:

  1. //建樹   
  2. Build(1,n,1);   
  3. //點修改  
  4. Update(L,C,1,n,1);  
  5. //區間修改   
  6. Update(L,R,C,1,n,1);  
  7. //區間查詢   
  8. int ANS=Query(L,R,1,n,1);  

感謝幾位網友指出了我的錯誤。

我說相對標記在Update時可以不下推,這一點是對的,但是原來的程式碼是錯誤的。

因為原來的程式碼中,PushUP函式是沒有考慮本節點的Add值的,如果Update時下推標記,那麼PushUp的時候,節點的Add值一定為零,所以不需要考慮Add。

但是,如果Update時暫時不下推標記的話,那麼PushUp函式就必須考慮本節點的Add值,否則會導致錯誤。

為了簡便,上面函式中,PushUp函式沒有考慮Add標記。所以無論是相對標記還是絕對標記,在更新資訊的時候,

到達的每個節點都必須呼叫PushDown函式來下推標記,另外,程式碼中,點修改函式中沒有PushDown函式,因為這裡假設只有點修改一種操作,

如果題目中是點修改和區間修改混合的話,那麼點修改中也需要PushDown。

四:非遞迴原理

非遞迴的思路很巧妙,思路以及部分程式碼實現 來自  清華大學 張昆瑋 《統計的力量》 ,有興趣可以去找來看。

非遞迴的實現,程式碼簡單(尤其是點修改和區間查詢),速度快,建樹簡單,遍歷元素簡單。總之能非遞迴就非遞迴吧。

不過,要支援區間修改的話,程式碼會變得複雜,所以區間修改的時候還是要取捨。有個特例,如果區間修改,但是隻需要

在所有操作結束之後,一次性下推所有標記,然後求結果,這樣的話,非遞迴寫起來也是很方便的。

下面先講思路,再講實現。

點修改:

非遞迴的思想總的來說就是自底向上進行各種操作。回憶遞迴線段樹的點修改,首先由根節點1向下遞迴,找到對應的葉

節點,然後,修改葉節點的值,再向上返回,在函式返回的過程中,更新路徑上的節點的統計資訊。而非遞迴線段樹的思路是,

如果可以直接找到葉節點,那麼就可以直接從葉節點向上更新,而一個節點找父節點是很容易的,編號除以2再下取整就行了。

那麼,如何可以直接找到葉節點呢?非遞迴線段樹擴充了普通線段樹(假設元素數量為n),使得所有非葉結點都有兩個子結點且葉子結點都在同一層。

來觀察一下擴充後的性質:

可以注意到紅色和黑色數字的差是固定的,如果事先算出這個差值,就可以直接找到葉節點。

注意:區分3個概念:原陣列下標,線段樹中的下標和儲存下標。

原陣列下標,是指,需要維護統計資訊(比如區間求和)的陣列的下標,這裡都預設下標從1開始(一般用A陣列表示)

線段樹下標,是指,加入線段樹中某個位置的下標,比如,原陣列中的第一個數,一般會加入到線段樹中的第二個位置,

為什麼要這麼做,後面會講。

儲存下標,是指該元素所在的葉節點的編號,即實際儲存的位置。

【在上面的圖片中,紅色為原陣列下標,黑色為儲存下標】

有了這3個概念,下面開始講區間查詢。

點修改下的區間查詢:

首先,區間的劃分沒有變,現在關鍵是如何直接找到被分成的區間。原來是遞迴查詢,判斷左右子區間跟[L,R]是否有交點,

若有交點則向下遞迴。現在要非遞迴實現,這就是巧妙之處,見下圖,以查詢[3,11]為例子。

其實,容易發現,紫色部分的變化,跟原來分析線段樹的區間分解的時候是一樣的規則,圖中多的藍色是什麼意思呢?

首先注意到,藍色節點剛好在紫色節點的兩端。

回憶一下,原來線段樹在區間逐層被替代的過程中,哪些節點被留了下來?最左側的節點,若為其父節點的右子節點,則留下。

最右側的節點,若為其父節點的左子節點則留下。那麼對於包裹著紫色的藍色節點來看,剛好相反。

比如,以左側的的藍色為例,若該節點是其父節點的右子節點,就證明它右側的那個紫色節點不會留下,會被其父替代,所以沒必要在這一步計算,若該節點是其父節點的左子節點,就證明它右側的那個紫色節點會留在這一層,所以必須在此刻計算,否則以後都不會再計算這個節點了。這樣逐層上去,容易發現,對於左側的藍色節點來說,只要它是左子節點,那麼就要計算對應的右子節點。同理,對於右側的藍色節點,只要它是右子節點,就需要計算它對應的左子節點。這個計算一直持續到左右藍色節點的父親為同一個的時候,才停止。於是,區間查詢,其實就是兩個藍色節點一路向上走,在路徑上更新答案。這樣,區間修改就變成了兩條同時向根走的鏈,明顯複雜度O(log2(n))。並且可以非遞迴實現。

至此,區間查詢也解決了,可以直接找到所有分解成的區間。

但是有一個問題,如果要查詢[1,5]怎麼辦?[1]左邊可是沒地方可以放置藍色節點了。

問題的解決辦法簡單粗暴,原陣列的1到n就不存線上段樹的1到n了,而是存線上段樹的2到n+1,

而開始要建立一顆有n+2個元素的樹,空出第一個和最後一個元素的空間。

現在來講如何對線段樹進行擴充。

再來看這個二叉樹,令N=8;注意到,該樹可以存8個元素,並且[1..7]是非葉節點,[8..15]是葉節點。

也就是說,左下角為N的二叉樹,可以存N個元素,並且[1..N-1]是非葉節點,[N..2N-1]是葉節點。

並且,線段樹下標+N-1=儲存下標 (還記不記得原來對三個下標的定義)

這時,這個線段樹存在兩段座標對映:

原陣列下標+1=線段樹下標

線段樹下標+N-1=儲存下標 

聯立方程得到:原陣列下標+N=儲存下標

於是從原陣列下標到儲存下標的轉換及其簡單。

下一個問題:N怎麼確定?

上面提到了,N的含義之一是,這棵樹可以存N個元素,也就是說N必須大於等於n+2

於是,N的定義,N是大於等於n+2的,某個2的次方。

區間修改下的區間查詢:

方法之一:如果題目許可,可以直接打上標記,最後一次下推所有標記,然後就可以遍歷葉節點來獲取資訊。

方法之二:如果題目查詢跟修改混在一起,那麼,採用標記永久化思想。也就是,不下推標記。

遞迴線段樹是在查詢區間的時候下推標記,使得到達每個子區間的時候,Sum已經是正確值。

非遞迴沒法這麼做,非遞迴是從下往上,遇到標記就更新答案。

這題是Add標記,一個區間Add標記表示這個區間所有元素都需要增加Add

Add含義不變,Add仍然表示本節點的Sum已經更新完畢,但是子節點的Sum仍需要更新.

現在就是如何在查詢的時候根據標記更新答案。

觀察下圖:

左邊的藍色節點從下往上走,在藍色節點到達[1,4]時,注意到,左邊藍色節點之前計算過的所有節點(即[3,4])都是目前藍色節點的子節點也就是說,當前藍色節點的Add是要影響這個節點已經計算過的所有數。多用一個變數來記錄這個藍色節點已經計算過多少個數,根據個數以及當前藍色節點的Add,來更新最終答案。

更新完答案之後,再加上[5,8]的答案,同時當前藍色節點計算過的個數要+4(因為[5,8]裡有4個數)

然後當這個節點到達[1,8]節點時,可以更新[1,8]的Add.

這裡,本來左右藍色節點相遇之後就不再需要計算了,但是由於有了Add標記,左右藍色節點的公共祖先上的Add標記會影響目前的所有數,所以還需要一路向上查詢到根,沿路根據Add更新答案。

區間修改:

這裡講完了查詢,再來講講修改

修改的時候,給某個區間的Add加上了C,這個區間的子區間向上查詢時,會經過這個節點,也就是會計算這個Add,但是

如果路徑經過這個區間的父節點,就不會計算這個節點的Add,也就會出錯。這裡其實跟遞迴線段樹一樣,改了某個區間的Add

仍需要向上更新所有包含這個區間的Sum,來保持上面所有節點的正確性。

五:非遞迴實現

以下以維護數列區間和的線段樹為例,演示最基本的非遞迴線段樹程式碼。

(0)定義

  1. //   
  2. #define maxn 100007  
  3. int A[maxn],n,N;//原陣列,n為原陣列元素個數 ,N為擴充元素個數   
  4. int Sum[maxn<<2];//區間和   
  5. int Add[maxn<<2];//懶惰標記   

(1)建樹:

  1. //  
  2. void Build(int n){  
  3.     //計算N的值   
  4.     N=1;while(N < n+2) N <<= 1;  
  5.     //更新葉節點   
  6.     for(int i=1;i<=n;++i) Sum[N+i]=A[i];//原陣列下標+N=儲存下標  
  7.     //更新非葉節點   
  8.     for(int i=N-1;i>0;--i){  
  9.         //更新所有非葉節點的統計資訊   
  10.         Sum[i]=Sum[i<<1]+Sum[i<<1|1];  
  11.         //清空所有非葉節點的Add標記   
  12.         Add[i]=0;  
  13.     }  
  14. }   

(2)點修改:

A[L]+=C

  1. //  
  2. void Update(int L,int C){  
  3.     for(int s=N+L;s;s>>=1){  
  4.         Sum[s]+=C;  
  5.     }  
  6. }   

(3)點修改下的區間查詢:

求A[L..R]的和(點修改沒有使用Add所以不需要考慮)

程式碼非常簡潔,也不難理解,

s和t分別代表之前的論述中的左右藍色節點,其餘的程式碼根據之前的論述應該很容易看懂了。

s^t^1 在s和t的父親相同時值為0,終止迴圈。

兩個if是判斷s和t分別是左子節點還是右子節點,根據需要來計算Sum

  1. //  
  2. int Query(int L,int R){  
  3.     int ANS=0;  
  4.     for(int s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1){  
  5.         if(~s&1) ANS+=Sum[s^1];  
  6.         if( t&1) ANS+=Sum[t^1];  
  7.     }  
  8.     return ANS;  
  9. }   

(4)區間修改:

A[L..R]+=C

  1. <span style="font-size:14px;">//  
  2. void Update(int L,int R,int C){  
  3.     int s,t,Ln=0,Rn=0,x=1;  
  4.     //Ln:  s一路走來已經包含了幾個數  
  5.     //Rn:  t一路走來已經包含了幾個數  
  6.     //x:   本層每個節點包含幾個數  
  7.     for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){  
  8.         //更新Sum  
  9.         Sum[s]+=C*Ln;  
  10.         Sum[t]+=C*Rn;  
  11.         //處理Add  
  12.         if(~s&1) Add[s^1]+=C,Sum[s^1]+=C*x,Ln+=x;  
  13.         if( t&1) Add[t^1]+=C,Sum[t^1]+=C*x,Rn+=x;  
  14.     }  
  15.     //更新上層Sum  
  16.     for(;s;s>>=1,t>>=1){  
  17.         Sum[s]+=C*Ln;  
  18.         Sum[t]+=C*Rn;  
  19.     }   
  20. } </span>  

(5)區間修改下的區間查詢:

求A[L..R]的和

  1. //  
  2. int Query(int L,int R){  
  3.     int s,t,Ln=0,Rn=0,x=1;  
  4.     int ANS=0;  
  5.     for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){  
  6.         //根據標記更新   
  7.         if(Add[s]) ANS+=Add[s]*Ln;  
  8.         if(Add[t]) ANS+=Add[t]*Rn;  
  9.         //常規求和   
  10.         if(~s&1) ANS+=Sum[s^1],Ln+=x;  
  11.         if( t&1) ANS+=Sum[t^1],Rn+=x;   
  12.     }  
  13.     //處理上層標記  
  14.     for(;s;s>>=1,t>>=1){  
  15.         ANS+=Add[s]*Ln;  
  16.         ANS+=Add[t]*Rn;  
  17.     }  
  18.     return ANS;  
  19. }  

六:線段樹解題模型

給出線段樹解題模型以及一些例題。

先對圖中各個名字給出定義:

問題:可能可以用線段樹解決的問題

目標資訊:由問題轉換而成的,為了解決問題而需要統計的資訊(可能不滿足區間加法)。

點資訊:每個點儲存的資訊

區間資訊:每個區間維護的資訊(線段樹節點定義) (必須滿足區間加法)

區間資訊包括 統計資訊標記

--------統計資訊:統計節點代表的區間的資訊,一般自下而上更新

--------標記:對操作進行標記(在區間修改時需要),一般自上而下傳遞,或者不傳遞

區間加法:實現區間加法的程式碼

查詢:實現查詢操作的程式碼

修改:實現修改操作的程式碼

圖中紫線右邊是實際線段樹的實現,左邊是對問題的分析以及轉換。

一個問題,若能轉換成對一些連續點的修改或者統計,就可以考慮用線段樹解決。

首先確定目標資訊點資訊,然後將目標資訊轉換成區間資訊(必要時,增加資訊,使之符合區間加法)。

之後就是線段樹的程式碼實現了,包括:

1.區間加法 

2.建樹,點資訊到區間資訊的轉換 

3.每種操作(包括查詢,修改)對區間資訊的呼叫,修改

這樣,點的資訊不同,區間資訊不同,線段樹可以維護很多種類的資訊,所以是一種非常實用的資料結構。

可以解決很多問題,下面給出幾個例子來說明。

(1):字串雜湊

題目:URAL1989 Subpalindromes    題解

給定一個字串(長度<=100000),有兩個操作。   1:改變某個字元。 2:判斷某個子串是否構成迴文串。 

直接判斷會超時。這個題目,是用線段樹維護字串雜湊

對於一個字串a[0],a[1],...,a[n-1] 它對應的雜湊函式為a[0]+a[1]*K + a[2]*K^2 +...+a[n-1]*K^(n-1)

再維護一個從右往左的雜湊值:a[0]*K^(n-1) + a[1]*K^(n-2) +...+a[n-1]

若是迴文串,則左右的雜湊值會相等。而左右雜湊值相等,則很大可能這是迴文串。

若出現誤判,可以再用一個K2,進行二次雜湊判斷,可以減小誤判概率。

實現上,雜湊值最好對某個質數取餘數,這樣分佈更均勻。

解題模型:

問題經過轉換之後:

目標資訊:某個區間的左,右雜湊值

點資訊:一個字元

目標資訊已經符合區間加法,所以區間資訊=目標資訊

所以線段樹的結構為:

區間資訊:區間雜湊值

點資訊:一個字元

程式碼主要需要注意2個部分:

1.區間加法 :(PushUp函式,Pow[a]=K^a)

2.點資訊->區間資訊:(葉節點上,區間只包含一個點,所以需要將點資訊轉換成區間資訊)

修改以及查詢,在有了區間加法的情況下,沒什麼難度了。

可以看出,上述解題過程的核心,就是找到區間資訊, 寫好區間加法

下面是維護區間和的部分,下面的程式碼沒有取餘,也就是實際上是對2^32取餘數,這樣其實分佈不均勻,容易出現誤判:

  1. //   
  2. #define K 137  
  3. #define maxn 100001   
  4. char str[maxn];  
  5. int Pow[maxn];//K的各個次方   
  6. struct Node{  
  7.     int KeyL,KeyR;  
  8.     Node():KeyL(0),KeyR(0){}  
  9.     void init(){KeyL=KeyR=0;}  
  10. }node[maxn<<2];  
  11. void PushUp(int L,int R,int rt){  
  12.     node[rt].KeyL=node[rt<<1].KeyL+node[rt<<1|1].KeyL*Pow[L];  
  13.     node[rt].KeyR=node[rt<<1].KeyR*Pow[R]+node[rt<<1|1].KeyR;  
  14. }  

(2):最長連續零

題目:Codeforces 527C Glass Carving   題解

題意是給定一個矩形,不停地縱向或橫向切割,問每次切割後,最大的矩形面積是多少。

最大矩形面積=最長的長*最寬的寬

這題,長寬都是10^5,所以,用01序列表示每個點是否被切割,然後,

最長的長就是長的最長連續0的數量+1

最長的寬就是寬的最長連續0的數量+1

於是用線段樹維護最長連續零

問題轉換成:

目標資訊:區間最長連續零的個數

點資訊:0 或 1

由於目標資訊不符合區間加法,所以要擴充目標資訊。

轉換後的線段樹結構

區間資訊:從左,右開始的最長連續零,本區間是否全零,本區間最長連續零。

點資訊:0 或 1

然後還是那2個問題:

1.區間加法:

這裡,一個區間的最長連續零,需要考慮3部分:

-(1):左子區間最長連續零

-(2):右子區間最長連續零

-(3):左右子區間拼起來,而在中間生成的連續零(可能長於兩個子區間的最長連續零)

而中間拼起來的部分長度,其實是左區間從右開始的最長連續零+右區間從左開始的最長連續零。

所以每個節點需要多兩個量,來存從左右開始的最長連續零。

然而,左開始的最長連續零分兩種情況,

--(1):左區間不是全零,那麼等於左區間的左最長連續零

--(2):左區間全零,那麼等於左區間0的個數加上右區間的左最長連續零

於是,需要知道左區間是否全零,於是再多加一個變數。

最終,通過維護4個值,達到了維護區間最長連續零的效果。

2.點資訊->區間資訊 : 

如果是0,那麼  最長連續零=左最長連續零=右最長連續零=1 ,全零=true。

如果是1,那麼  最長連續零=左最長連續零=右最長連續零=0, 全零=false。

至於修改和查詢,有了區間加法之後,機械地寫一下就好了。

由於這裡其實只有對整個區間的查詢,所以查詢函式是不用寫的,直接找根的統計資訊就行了。

程式碼如下:

  1. //  
  2. #define maxn 200001  
  3. using namespace std;  
  4. int L[maxn<<2][2];//從左開始連續零個數   
  5. int R[maxn<<2][2];//從右   
  6. int Max[maxn<<2][2];//區間最大連續零   
  7. bool Pure[maxn<<2][2];//是否全零   
  8. int M[2];  
  9. void PushUp(int rt,int k){//更新rt節點的四個資料  k來選擇兩棵線段樹  
  10.     Pure[rt][k]=Pure[rt<<1][k]&&Pure[rt<<1|1][k];   
  11.     Max[rt][k]=max(R[rt<<1][k]+L[rt<<1|1][k],max(Max[rt<<1][k],Max[rt<<1|1][k]));  
  12.     L[rt][k]=Pure[rt<<1][k]?L[rt<<1][k]+L[rt<<1|1][k]:L[rt<<1][k];  
  13.     R[rt][k]=Pure[rt<<1|1][k]?R[rt<<1|1][k]+R[rt<<1][k]:R[rt<<1|1][k];  
  14. }  

(3):計數排序

題目:Codeforces 558E A Simple Task  題解

給定一個長度不超過10^5的字串(小寫英文字母),和不超過5000個操作。

每個操作 L R K 表示給區間[L,R]的字串排序,K=1為升序,K=0為降序。

最後輸出最終的字串。

題目轉換成:

目標資訊:區間的計數排序結果

點資訊:一個字元

這裡,目標資訊是符合區間加法的,但是為了支援區間操作,還是需要擴充資訊。

轉換後的線段樹結構

區間資訊:區間的計數排序結果,排序標記,排序種類(升,降)

點資訊:一個字元

程式碼中需要解決的四個問題(難點在於標記下推和區間修改):

1.區間加法

對應的字元數量相加即可(注意標記是不上傳的,所以區間加法不考慮標記)。

2.點資訊->區間資訊:把對應字元的數量設定成1,其餘為0,排序標記為false。

3.標記下推

明顯,排序標記是絕對標記,也就是說,標記對子節點是覆蓋式的效果,一旦被打上標記,下層節點的一切資訊都無效。

下推標記時,根據自己的排序結果,將元素分成對應的部分,分別裝入兩個子樹。

4.區間修改

這個是難點,由於要對某個區間進行排序,首先對各個子區間求和(求和之前一定要下推標記,才能保證求的和是正確的)

由於使用的計數排序,所以求和之後,新順序也就出來了。然後按照排序的順序按照每個子區間的大小來分配字元。

操作後,每個子區間都被打上了標記。

最後,在所有操作結束之後,一次下推所有標記,就可以得到最終的字元序列。

這裡只給出節點定義。

  1. //   
  2. struct Node{  
  3.     int d[26];//計數排序   
  4.     int D;//總數  
  5.     bool sorted;//是否排好序   
  6.     bool Inc;//是否升序  
  7. };  

(4)總結:

總結一下,線段樹解題步驟。

:將問題轉換成點資訊目標資訊

即,將問題轉換成對一些點的資訊的統計問題。

:將目標資訊根據需要擴充成區間資訊

1.增加資訊符合區間加法。

2.增加標記支援區間操作。

:程式碼中的主要模組:

1.區間加法 

2.標記下推 

3.點資訊->區間資訊 

4.操作(各種操作,包括修改和查詢)

完成第一步之後,題目有了可以用線段樹解決的可能。

完成第二步之後,題目可以由線段樹解決。

第三步就是慢慢寫程式碼了。  

七:掃描線

線段樹的一大應用是掃描線。

先把相關題目給出,有興趣可以去找來練習:

POJ 1177 Picture:給定若干矩形求合併之後的圖形周長    題解

HDU 1255 覆蓋的面積:給定平面上若干矩形,求出被這些矩形覆蓋過至少兩次的區域的面積.   題解

HDU 3642 Get The Treasury:給定若干空間立方體,求重疊了3次或以上的體積(這個是掃描面,每個面再掃描線)題解

再補充一道稍微需要一點模型轉換的掃描線題:

POJ 2482 Stars in your window : 給定一些星星的位置和亮度,求用W*H的矩形能夠框住的星星亮度之和最大為多少。

這題是把星星轉換成了矩形,把矩形框轉換成了點,然後再掃描線。  題解

掃描線求重疊矩形面積:

考慮下圖中的四個矩形:

觀察第三個圖:

掃描線的思路:使用一條垂直於X軸的直線,從左到右來掃描這個圖形,明顯,只有在碰到矩形的左邊界或者右邊界的時候,

這個線段所掃描到的情況才會改變,所以把所有矩形的入邊,出邊按X值排序。然後根據X值從小到大去處理,就可以

用線段樹來維護掃描到的情況。如上圖,X1到X8是所有矩形的入邊,出邊的X座標。

而紅色部分的線段,是這樣,如果碰到矩形的入邊,就把這條邊加入,如果碰到出邊,就拿走。紅色部分就是有線段覆蓋的部分。

要求面積,只需要知道圖中的L1到L8。而線段樹就是用來維護這個L1到L8的。

掃描線演算法流程:

X1:首先遇到X1,將第一條線段加入線段樹,由線段樹統計得到線段長度為L1.

X2:然後繼續掃描到X2,此時要進行兩個動作:

1.計算面積,目前掃過的面積=L1*(X2-X1)

2.更新線段。由於X2處仍然是入邊,所以往線段樹中又加了一條線段,加的這條線段可以參考3幅圖中的第一幅。

然後線段樹自動得出此時覆蓋的線段長度為L2 (注意兩條線段有重疊部分,重疊部分的長度只能算一次)

X3:繼續掃描到X3,步驟同X2

先計算 掃過的面積+=L2*(X3-X2)

再加入線段,得到L3.

X4:掃描到X4有些不一樣了。

首先還是計算  掃過的面積+=L3*(X4-X3)

然後這時遇到了第一個矩形的出邊,這時要從線段樹中刪除一條線段。

刪除之後的結果是線段樹中出現了2條線段,線段樹自動維護這兩條線段的長度之和L4

講到這裡演算法流程應該很清晰了。

首先將所有矩形的入邊,出邊都存起來,然後根據X值排序。

這裡用一個結構體,來存這些資訊,然後排序。

  1. //  
  2. struct LINE{  
  3.     int x;//橫座標   
  4.     int y1,y2;//矩形縱向線段的左右端點   
  5.     bool In;//標記是入邊還是出邊   
  6.     bool operator < (const Line &B)const{return x < B.x;}  
  7. }Line[maxn];   

然後掃描的時候,需要兩個變數,一個叫PreL,存前一個x的操作結束之後的L值,和X,前一個橫座標。

假設一共有Ln條線段,線段下標從0開始,已經排好序。

那麼演算法大概是這樣:

  1. //  
  2. int PreL=0;//前一個L值,剛開始是0,所以第一次計算時不會引入誤差   
  3. int X;//X值   
  4. int ANS=0;//存累計面積  
  5. int I=0;//線段的下標   
  6. while(I < Ln){  
  7.     //先計算面積   
  8.     ANS+=PreL*(Line[I].x-X);  
  9.     X=Line[I].x;//更新X值  
  10.     //對所有X相同的線段進行操作   
  11.     while(I < Ln && Line[I].x == X){  
  12.         //根據入邊還是出邊來選擇加入線段還是移除線段   
  13.         if(Line[I].In) Cover(Line[I].y1,Line[I].y2-1,1,n,1);  
  14.         else         Uncover(Line[I].y1,Line[I].y2-1,1,n,1);  
  15.         ++I;  
  16.     }   
  17. }  

無論是求面積還是周長,掃描線的結構大概就是上面的樣子。

需要解決的幾個問題:

現在有兩點需要說明一下。

(1):線段樹進行線段操作時,每個點的含義(比如為什麼Cover函式中,y2後面要-1)。

(2):線段樹如何維護掃描線過程中的覆蓋線段長度。

(3):線段樹如何維護掃描線過程中線段的數量。

(1):線段樹中點的含義

線段樹如果沒有離散化,那麼線段樹下標為1,就代表線段[1,2)

線段樹下標為K的時候,代表的線段為[K,K+1) (長度為1)

所以,將上面的所有線段都化為[y1,y2)就可以理解了,線段[y1,y2)只包括線段樹下標中的y1,y1+1,...,y2-1

當y值的範圍是10^9時,就不能再按照上面的辦法按值建樹了,這時需要離散化。

下面是離散化的程式碼:

  1. //  
  2. int Rank[maxn],Rn;  
  3. void SetRank(){//呼叫前,所有y值被無序存入Rank陣列,下標為[1..Rn]   
  4.     int I=1;  
  5.     //第一步排序   
  6.     sort(Rank+1,Rank+1+Rn);   
  7.     //第二步去除重複值   
  8.     for(int i=2;i<=Rn;++i) if(Rank[i]!=Rank[i-1]) Rank[++I]=Rank[i];  
  9.     Rn=I;  
  10.     //此時,所有y值被從小到大無重複地存入Rank陣列,下標為[1..Rn]   
  11. }   
  12. int GetRank(int x){//給定x,求x的下標   
  13.     //二分法求下標   
  14.     int L=1,R=Rn,M;//[L,R] first >=x  
  15.     while(L!=R){  
  16.         M=(L+R)>>1;  
  17.         if(Rank[M]<x) L=M+1;  
  18.         else R=M;  
  19.     }  
  20.     return L;  
  21. }  

此時,線段樹的下標的含義就變成:如果線段樹下標為K,代表線段[ Rank[K] , Rank[K+1] )。

下標為K的線段長度為Rank[K+1]-Rank[K]

所以此時葉節點的線段長度不是1了。

這時,之前的掃描線演算法的函式呼叫部分就稍微的改變了一點:

  1. //   
  2. if(Line[I].In) Cover(GetRank(Line[I].y1),GetRank(Line[I].y2)-1,1,n,1);  
  3. else         Uncover(GetRank(Line[I].y1),GetRank(Line[I].y2)-1,1,n,1);  

看著有點長,其實不難理解,只是多了一步從y值到離散之後的下標的轉換。

注意一點,如果下標為K的線段長度為Rank[K+1]-Rank[K],那麼下標為Rn的線段樹的長度呢?

其實這個不用擔心,Rank[Rn]作為所有y值中的最大值,它肯定是一個線段的右端點,

而右端點求完離散之後的下標還要-1,所以上面的線段覆蓋永遠不會覆蓋到Rn。

所以線段樹其實只需要建立Rn-1個元素,因為下標為Rn的無法定義,也不會被訪問。

不過有時候留著也有好處,這個看具體實現時自己取捨。

(2):如何維護覆蓋線段長度

先提一個小技巧,一般,利用兩個子節點來更新本節點的函式寫成PushUp();

但是,對於比較複雜的子區間合併問題,在區間查詢的時候,需要合併若干個子區間。

而合併子區間是沒辦法用PushUp函式的。於是,對於比較複雜的問題,把單個節點的資訊寫成一個結構體。

在結構體內過載運算子"+",來實現區間合併。這樣,不僅在PushUp函式可以呼叫這個加法,區間詢問時也可以

呼叫這個加法,這樣更加方便。

下面給出維護線段覆蓋長度的節點定義:

  1. //  
  2. struct Node{  
  3.     int Cover;//區間整體被覆蓋的次數   
  4.     int L;//Length : 所代表的區間總長度  
  5.     int CL;//Cover Length :實際覆蓋長度  
  6.     Node operator +(const Node &B)const{  
  7.         Node X;  
  8.         X.Cover=0;//因為若上級的Cover不為0,不會呼叫子區間加法函式   
  9.         X.L=L+B.L;  
  10.         X.CL=CL+B.CL;  
  11.         return X;  
  12.     }  
  13. }K[maxn<<2];  

這樣定義之後,區間的資訊更新是這樣的:

若本區間的覆蓋次數大於0,那麼令CL=L,直接為全覆蓋,不管下層是怎麼覆蓋的,反正本區間已經全被覆蓋。

若本區間的覆蓋次數等於0,那麼呼叫上面結構體中的加法函式,利用子區間的覆蓋來計算。

加入一條線段就是給每一個分解的子區間的Cover+1,刪除線段就-1,每次修改Cover之後,更新區間資訊。

這裡完全沒有下推標記的過程。

查詢的程式碼如下:

如果不把區間加法定義成結構體內部的函式,而是定義在PushUp函式內,那麼這裡幾乎就要重寫一遍區間合併。

因為PushUp在這裡用不上。

  1. //  
  2. Node Query(int L,int R,int l,int r,int rt){  
  3.     if(L <= l && r <= R){  
  4.         return K[rt];  
  5.     }  
  6.     int m=(l+r)>>1;  
  7.     Node LANS,RANS;  
  8.     int X=0;  
  9.     if(L <= m) LANS=Query(L,R,ls),X+=1;  
  10.     if(R >  m) RANS=Query(L,R,rs),X+=2;  
  11.     if(X==1) return LANS;  
  12.     if(X==2) return RANS;  
  13.     return LANS+RANS;  
  14. }  

維護線段覆蓋3次或以上的長度:

  1. //  
  2. struct Nodes{    
  3.     int C;//Cover    
  4.     int CL[4];//CoverLength[0~3]    
  5.     //CL[i]表示被覆蓋了大於等於i次的線段長度,CL[0]其實就是線段總長   
  6. }ST[maxn<<2];    
  7. void PushUp(int rt){    
  8.     for(int i=1;i<=3;++i){    
  9.         if(ST[rt].C < i) ST[rt].CL[i]=ST[rt<<1].CL[i-ST[rt].C]+ST[rt<<1|1].CL[i-ST[rt].C];    
  10.         else ST[rt].CL[i]=ST[rt].CL[0];    
  11.     }    
  12. }    

這裡給出節點定義和PushUp().

更新節點資訊的思路大概就是:

假設要更新CL[3],然後發現本節點被覆蓋了2次,那麼本節點被覆蓋三次或以上的長度就等於子節點被覆蓋了1次或以上的長度之和。

而CL[0]建樹時就賦值,之後不需要修改。

(3):如何維護掃描線過程中線段的數量

  1. //   
  2. struct Node{    
  3.     int cover;//完全覆蓋層數    
  4.     int lines;//分成多少個線段    
  5.     bool L,R;//左右端點是否被覆蓋    
  6.     Node operator +(const Node &B){//連續區間的合併     
  7.         Node C;    
  8.         C.cover=0;    
  9.         C.lines=lines+B.lines-(R&&B.L);    
  10.         C.L=L;C.R=B.R;    
  11.         return C;    
  12.     }    
  13. }K[maxn<<2];    

要維護被分成多少個線段,就需要記錄左右端點是否被覆蓋,知道了這個,就可以合併區間了。

左右兩個區間合併時,若左區間的最右側有線段且右區間的最左側也有線段,那麼這兩個線段會合二為一,於是匯流排段數量會少1.

掃描線求重疊矩形周長:

這個圖是在原來的基礎上多畫了一些東西,這次是要求周長。

所有的橫向邊都畫了紫色,所有的縱向邊畫了綠色。

先考慮綠色的邊,由圖可以觀察到,綠色邊的長度其實就是L的變化值。

比如考慮X1,本來L是0,從0變到L1,所以綠色邊長為L1.

再考慮X2,由L1變成了L2,所以綠色邊長度為L2-L1,

於是,綠色邊的長度就是L的變化值(注意上圖中令L0=0,L9=0)。

因為長度是從0開始變化,最終歸0.

再考慮紫色的邊,要計算紫色邊,其實就是計算L的線段是有幾個線段組成的,每個線段會貢獻兩個端點(紫色圓圈)

而每個端點都會向右延伸出一條紫色邊一直到下一個X值。

所以周長就是以上兩部分的和。而兩部分怎麼維護,前面都講過了,下面給出程式碼。

  1. //   
  2. struct Node{    
  3.     int cover;//完全覆蓋層數    
  4.     int lines;//分成多少個線段    
  5.     bool L,R;//左右端點是否被覆蓋    
  6.     int CoverLength;//覆蓋長度     
  7.     int Length;//總長度     
  8.     Node(){}    
  9.     Node(int cover,int lines,bool L,bool R,int CoverLength):cover(cover),lines(lines),L(L),R(R),CoverLength(CoverLength){}    
  10.     Node operator +(const Node &B){//連續區間的合併     
  11.         Node C;    
  12.         C.cover=0;    
  13.         C.lines=lines+B.lines-(R&&B.L);    
  14.         C.CoverLength=CoverLength+B.CoverLength;    
  15.         C.L=L;C.R=B.R;    
  16.         C.Length=Length+B.Length;    
  17.         return C;    
  18.     }    
  19. }K[maxn<<2];    
  20. void PushUp(int rt){//更新非葉節點     
  21.     if(K[rt].cover){    
  22.         K[rt].CoverLength=K[rt].Length;    
  23.         K[rt].L=K[rt].R=K[rt].lines=1;    
  24.     }    
  25.     else{    
  26.         K[rt]=K[rt<<1]+K[rt<<1|1];    
  27.     }    
  28. }    

掃描的程式碼:

  1. int PreX=L[0].x;//前X座標     
  2. int ANS=0;//目前累計答案     
  3. int PreLength=0;//前線段總長    
  4. int PreLines=0;//前線段數量     
  5. Build(1,20001,1);    
  6. for(int i=0;i<nL;++i){    
  7.     //操作     
  8.     if(L[i].c) Cover(L[i].y1,L[i].y2-1,1,20001,1);    
  9.     else Uncover(L[i].y1,L[i].y2-1,1,20001,1);    
  10.     //更新橫向的邊界     
  11.     ANS+=2*PreLines*(L[i].x-PreX);    
  12.     PreLines=K[1].lines;    
  13.     PreX=L[i].x;    
  14.     //更新縱向邊界     
  15.     ANS+=abs(K[1].CoverLength-PreLength);    
  16.     PreLength=K[1].CoverLength;    
  17. }    
  18. //輸出答案     
  19. printf("%d\n",ANS);    

求立方體重疊3次或以上的體積:

這個首先掃描面,每個面內求重疊了3次或以上的面積,然後乘以移動距離就是體積。

面內掃描線,用線段樹維護重疊了3次或以上的線段長度,然後用長度乘移動距離就是重疊了3次或以上的面積。

掃描面基本原理都跟掃描線一樣,就是嵌套了一層而已,寫的時候細心一點就沒問題了。

八:可持久化 (主席樹)

可持久化線段樹,也叫主席樹。

可持久化資料結構思想,就是保留整個操作的歷史,即,對一個線段樹進行操作之後,保留訪問操作前的線段樹的能力。

最簡單的方法,每操作一次,建立一顆新樹。這樣對空間的需求會很大。

而注意到,對於點修改,每次操作最多影響個節點,於是,其實操作前後的兩個線段樹,結構一樣,

而且只有個節點不同,其餘的節點都一樣,於是可以重複利用其餘的點。

這樣,每次操作,會增加個節點。

於是,這樣的線段樹,每次操作需要O(log2(n))的空間。

題目:HDU 2665 Kth number      題解

給定10萬個數,10萬個詢問。

每個詢問,問區間[L,R]中的數,從小到大排列的話,第k個數是什麼。

這個題,首先對十萬個數進行離散化,然後用線段樹來維護數字出現的次數。

每個節點都存出現次數,那麼查詢時,若左節點的數的個數>=k,就往左子樹遞迴,否則往右子樹遞迴。

一直到葉節點,就找到了第k大的數。

這題的問題是,怎麼得到一個區間的每個數出現次數。

注意到,數字的出現次數是滿足區間減法的。

於是要求區間[L,R]的數,其實就是T[R]-T[L-1]  ,其中T[X]表示區間[1,X]的數形成的線段樹。

現在的問題就是,如何建立這10萬個線段樹。

由之前的分析,需要O(n log2(n))的空間

下面是程式碼:

  1. //主席樹     
  2. int L[maxnn],R[maxnn],Sum[maxnn],T[maxn],TP;//左右子樹,總和,樹根,指標     
  3. void Add(int &rt,int l,int r,int x){//建立新樹,l,r是區間, x是新加入的數字的排名  
  4.     ++TP;L[TP]=L[rt];R[TP]=R[rt];Sum[