線段樹詳解 (原理,實現與應用)

原文鏈接:https://blog.csdn.net/zearot/article/details/48299459
線段樹詳解

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] 。


(2.2)然後給出正式一點的證明:
定理:n>=3時,一個[1,n]的線段樹可以將[1,n]的任意子區間[L,R]分解爲不超過個子區間。

用數學歸納法,證明上面的定理:
首先,n=3,4,5時,用窮舉法不難證明定理成立。
假設對於n= 3,4,5,...,k-1上式都成立,下面來證明對於n=k ( k>=6 )成立:
分爲4種情況來證明:

情況一:[L,R]包含根節點(L=1且R=n),此時,[L,R]被分解爲了一個節點,定理成立。

情況二:[L,R]包含根節點的左子節點,此時[L,R]一定不包含根的右子節點(因爲如果包含,就可以合併左右子節點,
用根節點替代,此時就是情況一)。這時,以右子節點爲根的這個樹的元素個數爲
[L,R]分成的子區間由兩部分組成:
一:根的左子結點,區間數爲1 
二:以根的右子節點爲根的樹中,進行區間查詢,這個可以遞歸使用本定理。
由歸納假設可得,[L,R]一共被分成了個區間。
情況三:跟情況二對稱,不一樣的是,以根的左子節點爲根的樹的元素個數爲
[L,R]一共被分成了個區間。
從公式可以看出,情況二的區間數小於等於情況三的區間數,於是只需要證明情況三的區間數符合條件就行了。
於是,情況二和情況三定理成立。

情況四:[L,R]不包括根節點以及根節點的左右子節點。
於是,剩下的層,每層最多兩個節點(參考粗略證明中的內容)。
於是[L,R]最多被分解成了個區間,定理成立。


上面只證明了是上界,但是,其實它是最小上界。
n=3,4時,有很多組區間的分解可以達到最小上界。
當n>4時,當且僅當n=2^t (t>=3),L=2,R=2^t -1 時,區間[L,R]的分解可以達到最小上界
就不證明了,有興趣可以自己去證明。
下圖是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[TP]=Sum[rt]+1;rt=TP;//複製&新建
  5. if(l==r) return;
  6. int m=(l+r)>>1;
  7. if(x <= m) Add(L[rt],l,m,x);
  8. else Add(R[rt],m+1,r,x);
  9. }
  10. int Search(int TL,int TR,int l,int r,int k){//區間查詢第k大
  11. if(l==r) return l;//返回第k大的下標
  12. int m=(l+r)>>1;
  13. if(Sum[L[TR]]-Sum[L[TL]]>=k) return Search(L[TL],L[TR],l,m,k);
  14. else return Search(R[TL],R[TR],m+1,r,k-Sum[L[TR]]+Sum[L[TL]]);
  15. }

以上就是主席樹部分的代碼。
熟悉SBT的,應該都很熟悉這種表示方法。
L,R是僞指針,指向左右子節點。
特殊之處是,0 表示空樹,並且 L[0]=R[0]=0.
也就是說,空樹的左右子樹都是空樹。
而本題中,每一顆樹其實都是完整的,剛開始有一顆空樹。
但是剛開始的空樹,真的需要用空間去存嗎?
其實不需要,剛開始的空樹有這些性質:
1.每個節點的Sum值爲0
2.每個非葉節點的左右子節點的Sum值也是0

而SBT的空樹剛好滿足這個性質。而線段樹不依賴L,R指針來結束遞歸。
線段樹是根據區間l,r來結束的,所以不會出現死循環。
所以只需要把Sum[0]=0;那麼剛開始就不需要建樹了,只有每個操作的個節點。

這個線段樹少了表示父節點的int rt,因爲不需要(也不能夠)通過rt來找子節點了,而是直接根據L,R來找。

-----------------------------     補充     -------------------------------------

終於又找到一道可以用主席樹的題目了:Codeforces 650D.Zip-line  題解
做這題之前需要會求普通的LIS問題(最長上升子序列問題)。

九:練習題

適合非遞歸線段樹的題目:


Codeforces 612D The Union of k-Segments :  題解
題意:線段求交,給定一堆線段,按序輸出被覆蓋k次或以上的線段和點。
基礎題,先操作,最後一次下推標記,然後輸出,
維護兩個線段樹,一個線段覆蓋,一個點覆蓋。

Codeforces 35E Parade : 題解

題意:給定若干矩形,下端挨着地面,求最後的輪廓形成的折線,要求輸出每一點的座標。

思路:雖然是區間修改的線段樹,但只需要在操作結束後一次下推標記,然後輸出,所以適合非遞歸線段樹。


URAL 1846 GCD2010 :  題解

題意:總共10萬個操作,每次向集合中加入或刪除一個數,求集合的最大公因數。(規定空集的最大公因數爲1)


Codeforces 12D Ball :   題解

題意:

給N (N<=500000)個點,每個點有x,y,z ( 0<= x,y,z <=10^9 )

對於某點(x,y,z),若存在一點(x1,y1,z1)使得x1 > x && y1 > y && z1 > z 則點(x,y,z)是特殊點。

問N個點中,有多少個特殊點。


提示:排序+線段樹


Codeforces 19D Points : 題解

題意:

給定最多20萬個操作,共3種:

1.add x y         :加入(x,y)這個點

2.remove x y  :刪除(x,y)這個點

3.find x y         :找到在(x,y)這點右上方的x最小的點,若x相同找y最小的點,輸出這點座標,若沒有,則輸出-1.

提示:排序,線段樹套平衡樹


Codeforces 633E Startup Funding : 題解

這題需要用到一點概率論,組合數學知識,和二分法。

非遞歸線段樹在這題中主要解決RMQ問題(區間最大最小值問題),由於不帶修改,這題用Sparse Table求解RMQ是標答。

因爲RMQ詢問是在二分法之內求的,而Sparse Table可以做到O(1)查詢,所以用Sparse Table比較好,總複雜度O(n*log(n))。

不過非遞歸線段樹也算比較快的了,雖然複雜度是O(n*log(n)*log(n)),還是勉強過了這題。


掃描線題目:

POJ 1177 Picture:給定若干矩形求合併之後的圖形周長    題解
HDU 1255 覆蓋的面積:給定平面上若干矩形,求出被這些矩形覆蓋過至少兩次的區域的面積.   題解
HDU 3642 Get The Treasury:給定若干空間立方體,求重疊了3次或以上的體積(這個是掃描面,每個面再掃描線)題解
POJ 2482 Stars in your window : 給定一些星星的位置和亮度,求用W*H的矩形能夠框住的星星亮度之和最大爲多少。  題解

遞歸線段樹題目:

Codeforces 558E A Simple Task  題解

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

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

最後輸出最終的字符串。


Codeforces 527C Glass Carving  :  題解
給定一個矩形,不停地縱向或橫向切割,問每次切割後,最大的矩形面積是多少。

URAL1989 Subpalindromes    題解
給定一個字符串(長度<=100000),有10萬個操作。
操作有兩種:   
1:改變某個字符。 
2:判斷某個子串是否構成迴文串。 

HDU 4288 Coder :  題解
 題意:對一個集合進行插入與刪除操作。要求詢問某個時刻,集合中的元素從小到大排序之後,序號%5 ==3 的元素值之和。
這題其實不一定要用線段樹去做的,不過線段樹還是可以做的。

HDU 2795 BillBoard : 題解

題意:有一個板,h行,每行w長度的位置。每次往上面貼一張海報,長度爲1*wi .

每次貼的時候,需要找到最上面的,可以容納的空間,並且靠邊貼。


Codeforces 374D Inna and Sequence題解
題意:給定百萬個數a[m],然後有萬個操作,每次給現有序列加一個字符(0或1),或者刪掉已有序列中,第 a[0] 個,第a[1]個,...,第a[m]個。

Codeforces 482B Interesting Array:  題解

題意就是,給定n,m.

滿足m個條件的n個數,或說明不存在。

每個條件的形式是,給定 Li,Ri,Qi ,要求   a[Li]&a[Li+1]&...&a[Ri] = Qi ;

Codeforces 474E Pillar (線段樹+動態規劃):  題解

題意就是,給定10^5 個數(範圍10^15),求最長子序列使得相鄰兩個數的差大於等於 d。


POJ 2777  Count Color :   題解

給線段塗顏色,最多30種顏色,10萬個操作。

每個操作給線段塗色,或問某一段線段有多少種顏色。

30種顏色用int的最低30位來存,然後線段樹解決。


URAL 1019 Line Painting: 線段樹的區間合併  題解

給一段線段進行黑白塗色,最後問最長的一段白色線段的長度。


Codeforces 633H Fibonacci-ish II  :題解

這題需要用到莫隊算法(Mo's Algorithm)+線段樹區間修改,不過是單邊界的區間,寫起來挺有趣。

另一種解法就是暴力,很巧妙的方法,高複雜度+低常數居然就這麼給過了。


樹套樹題目:

ZOJ 2112 Dynamic Rankings 動態區間第k大  題解
做法:樹狀數組套主席樹 或者 線段樹套平衡樹

Codeforces 605D Board Game :  題解
做法:廣度優先搜索(BFS)  +  線段樹套平衡樹

Codeforces 19D Points : 題解

題意:

給定最多20萬個操作,共3種:

1.add x y         :加入(x,y)這個點

2.remove x y  :刪除(x,y)這個點

3.find x y         :找到在(x,y)這點右上方的x最小的點,若x相同找y最小的點,輸出這點座標,若沒有,則輸出-1.

提示:排序,線段樹套平衡樹



轉載請註明出處: 原文地址:http://blog.csdn.net/zearot/article/details/48299459

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章