線段樹詳解
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)定義:
- #define maxn 100007 //元素總個數
- #define ls l,m,rt<<1
- #define rs m+1,r,rt<<1|1
- int Sum[maxn<<2],Add[maxn<<2];
- int A[maxn],n;
(1)建樹:
- void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}
- void Build(int l,int r,int rt){
- if(l==r) {
- Sum[rt]=A[l];
- 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){
- 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);
- }
(3)區間修改:
假設A[L,R]+=C
- void Update(int L,int R,int C,int l,int r,int rt){
- if(L <= l && r <= R){
- Sum[rt]+=C*(r-l+1);
- Add[rt]+=C;
- return ;
- }
- int m=(l+r)>>1;
- PushDown(rt,m-l+1,r-m);
-
- if(L <= m) Update(L,R,C,l,m,rt<<1);
- if(R > m) Update(L,R,C,m+1,r,rt<<1|1);
- PushUp(rt);
- }
(4)區間查詢:
詢問A[L,R]的和
首先是下推標記的函數:
- void PushDown(int rt,int ln,int rn){
-
- if(Add[rt]){
-
- Add[rt<<1]+=Add[rt];
- Add[rt<<1|1]+=Add[rt];
-
- Sum[rt<<1]+=Add[rt]*ln;
- Sum[rt<<1|1]+=Add[rt]*rn;
-
- Add[rt]=0;
- }
- }
然后是區間查詢的函數:
- int Query(int L,int R,int l,int r,int rt){
- if(L <= l && r <= R){
-
- return Sum[rt];
- }
- int m=(l+r)>>1;
-
- PushDown(rt,m-l+1,r-m);
-
-
- int ANS=0;
- if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
- if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
- return ANS;
- }
(5)函數調用:
- Build(1,n,1);
- Update(L,C,1,n,1);
- Update(L,R,C,1,n,1);
- 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)定義:
- #define maxn 100007
- int A[maxn],n,N;
- int Sum[maxn<<2];
- int Add[maxn<<2];
(1)建樹:
- void Build(int n){
-
- N=1;while(N < n+2) N <<= 1;
-
- for(int i=1;i<=n;++i) Sum[N+i]=A[i];
-
- for(int i=N-1;i>0;--i){
-
- Sum[i]=Sum[i<<1]+Sum[i<<1|1];
-
- Add[i]=0;
- }
- }
(2)點修改:
A[L]+=C
- void Update(int L,int C){
- for(int s=N+L;s;s>>=1){
- Sum[s]+=C;
- }
- }
(3)點修改下的區間查詢:
求A[L..R]的和(點修改沒有使用Add所以不需要考慮)
代碼非常簡潔,也不難理解,
s和t分別代表之前的論述中的左右藍色節點,其余的代碼根據之前的論述應該很容易看懂了。
s^t^1 在s和t的父親相同時值為0,終止循環。
兩個if是判斷s和t分別是左子節點還是右子節點,根據需要來計算Sum
- int Query(int L,int R){
- int ANS=0;
- for(int s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1){
- if(~s&1) ANS+=Sum[s^1];
- if( t&1) ANS+=Sum[t^1];
- }
- return ANS;
- }
(4)區間修改:
A[L..R]+=C
- <span style="font-size:14px;">
- void Update(int L,int R,int C){
- int s,t,Ln=0,Rn=0,x=1;
-
-
-
- for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){
-
- Sum[s]+=C*Ln;
- Sum[t]+=C*Rn;
-
- if(~s&1) Add[s^1]+=C,Sum[s^1]+=C*x,Ln+=x;
- if( t&1) Add[t^1]+=C,Sum[t^1]+=C*x,Rn+=x;
- }
-
- for(;s;s>>=1,t>>=1){
- Sum[s]+=C*Ln;
- Sum[t]+=C*Rn;
- }
- } </span>
(5)區間修改下的區間查詢:
求A[L..R]的和
- int Query(int L,int R){
- int s,t,Ln=0,Rn=0,x=1;
- int ANS=0;
- for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){
-
- if(Add[s]) ANS+=Add[s]*Ln;
- if(Add[t]) ANS+=Add[t]*Rn;
-
- if(~s&1) ANS+=Sum[s^1],Ln+=x;
- if( t&1) ANS+=Sum[t^1],Rn+=x;
- }
-
- for(;s;s>>=1,t>>=1){
- ANS+=Add[s]*Ln;
- ANS+=Add[t]*Rn;
- }
- return ANS;
- }
六:線段樹解題模型
給出線段樹解題模型以及一些例題。
先對圖中各個名字給出定義:
問題:可能可以用線段樹解決的問題
目標信息:由問題轉換而成的,為了解決問題而需要統計的信息(可能不滿足區間加法)。
點信息:每個點儲存的信息
區間信息:每個區間維護的信息(線段樹節點定義) (必須滿足區間加法)
區間信息包括 統計信息和標記
--------統計信息:統計節點代表的區間的信息,一般自下而上更新
--------標記:對操作進行標記(在區間修改時需要),一般自上而下傳遞,或者不傳遞
區間加法:實現區間加法的代碼
查詢:實現查詢操作的代碼
修改:實現修改操作的代碼
圖中紫線右邊是實際線段樹的實現,左邊是對問題的分析以及轉換。
一個問題,若能轉換成對一些連續點的修改或者統計,就可以考慮用線段樹解決。
首先確定目標信息和點信息,然后將目標信息轉換成區間信息(必要時,增加信息,使之符合區間加法)。
之后就是線段樹的代碼實現了,包括:
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取余數,這樣其實分布不均勻,容易出現誤判:
- #define K 137
- #define maxn 100001
- char str[maxn];
- int Pow[maxn];
- struct Node{
- int KeyL,KeyR;
- Node():KeyL(0),KeyR(0){}
- void init(){KeyL=KeyR=0;}
- }node[maxn<<2];
- void PushUp(int L,int R,int rt){
- node[rt].KeyL=node[rt<<1].KeyL+node[rt<<1|1].KeyL*Pow[L];
- node[rt].KeyR=node[rt<<1].KeyR*Pow[R]+node[rt<<1|1].KeyR;
- }
(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。
至於修改和查詢,有了區間加法之后,機械地寫一下就好了。
由於這里其實只有對整個區間的查詢,所以查詢函數是不用寫的,直接找根的統計信息就行了。
代碼如下:
- #define maxn 200001
- using namespace std;
- int L[maxn<<2][2];
- int R[maxn<<2][2];
- int Max[maxn<<2][2];
- bool Pure[maxn<<2][2];
- int M[2];
- void PushUp(int rt,int k){
- Pure[rt][k]=Pure[rt<<1][k]&&Pure[rt<<1|1][k];
- Max[rt][k]=max(R[rt<<1][k]+L[rt<<1|1][k],max(Max[rt<<1][k],Max[rt<<1|1][k]));
- L[rt][k]=Pure[rt<<1][k]?L[rt<<1][k]+L[rt<<1|1][k]:L[rt<<1][k];
- R[rt][k]=Pure[rt<<1|1][k]?R[rt<<1|1][k]+R[rt<<1][k]:R[rt<<1|1][k];
- }
(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.區間修改
這個是難點,由於要對某個區間進行排序,首先對各個子區間求和(求和之前一定要下推標記,才能保證求的和是正確的)
由於使用的計數排序,所以求和之后,新順序也就出來了。然后按照排序的順序按照每個子區間的大小來分配字符。
操作后,每個子區間都被打上了標記。
最后,在所有操作結束之后,一次下推所有標記,就可以得到最終的字符序列。
這里只給出節點定義。
- struct Node{
- int d[26];
- int D;
- bool sorted;
- bool Inc;
- };
(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值排序。
這里用一個結構體,來存這些信息,然后排序。
- struct LINE{
- int x;
- int y1,y2;
- bool In;
- bool operator < (const Line &B)const{return x < B.x;}
- }Line[maxn];
然后掃描的時候,需要兩個變量,一個叫PreL,存前一個x的操作結束之后的L值,和X,前一個橫坐標。
假設一共有Ln條線段,線段下標從0開始,已經排好序。
那么算法大概是這樣:
- int PreL=0;
- int X;
- int ANS=0;
- int I=0;
-
- while(I < Ln){
-
- ANS+=PreL*(Line[I].x-X);
- X=Line[I].x;
-
- while(I < Ln && Line[I].x == X){
-
- if(Line[I].In) Cover(Line[I].y1,Line[I].y2-1,1,n,1);
- else Uncover(Line[I].y1,Line[I].y2-1,1,n,1);
- ++I;
- }
- }
無論是求面積還是周長,掃描線的結構大概就是上面的樣子。
需要解決的幾個問題:
現在有兩點需要說明一下。
(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時,就不能再按照上面的辦法按值建樹了,這時需要離散化。
下面是離散化的代碼:
- int Rank[maxn],Rn;
- void SetRank(){
- int I=1;
-
- sort(Rank+1,Rank+1+Rn);
-
- for(int i=2;i<=Rn;++i) if(Rank[i]!=Rank[i-1]) Rank[++I]=Rank[i];
- Rn=I;
-
- }
- int GetRank(int x){
-
- int L=1,R=Rn,M;
- while(L!=R){
- M=(L+R)>>1;
- if(Rank[M]<x) L=M+1;
- else R=M;
- }
- return L;
- }
此時,線段樹的下標的含義就變成:如果線段樹下標為K,代表線段[ Rank[K] , Rank[K+1] )。
下標為K的線段長度為Rank[K+1]-Rank[K]
所以此時葉節點的線段長度不是1了。
這時,之前的掃描線算法的函數調用部分就稍微的改變了一點:
- if(Line[I].In) Cover(GetRank(Line[I].y1),GetRank(Line[I].y2)-1,1,n,1);
- 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函數可以調用這個加法,區間詢問時也可以
調用這個加法,這樣更加方便。
下面給出維護線段覆蓋長度的節點定義:
- struct Node{
- int Cover;
- int L;
- int CL;
- Node operator +(const Node &B)const{
- Node X;
- X.Cover=0;
- X.L=L+B.L;
- X.CL=CL+B.CL;
- return X;
- }
- }K[maxn<<2];
這樣定義之后,區間的信息更新是這樣的:
若本區間的覆蓋次數大於0,那么令CL=L,直接為全覆蓋,不管下層是怎么覆蓋的,反正本區間已經全被覆蓋。
若本區間的覆蓋次數等於0,那么調用上面結構體中的加法函數,利用子區間的覆蓋來計算。
加入一條線段就是給每一個分解的子區間的Cover+1,刪除線段就-1,每次修改Cover之后,更新區間信息。
這里完全沒有下推標記的過程。
查詢的代碼如下:
如果不把區間加法定義成結構體內部的函數,而是定義在PushUp函數內,那么這里幾乎就要重寫一遍區間合並。
因為PushUp在這里用不上。
- Node Query(int L,int R,int l,int r,int rt){
- if(L <= l && r <= R){
- return K[rt];
- }
- int m=(l+r)>>1;
- Node LANS,RANS;
- int X=0;
- if(L <= m) LANS=Query(L,R,ls),X+=1;
- if(R > m) RANS=Query(L,R,rs),X+=2;
- if(X==1) return LANS;
- if(X==2) return RANS;
- return LANS+RANS;
- }
維護線段覆蓋3次或以上的長度:
- struct Nodes{
- int C;
- int CL[4];
-
- }ST[maxn<<2];
- void PushUp(int rt){
- for(int i=1;i<=3;++i){
- 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];
- else ST[rt].CL[i]=ST[rt].CL[0];
- }
- }
這里給出節點定義和PushUp().
更新節點信息的思路大概就是:
假設要更新CL[3],然后發現本節點被覆蓋了2次,那么本節點被覆蓋三次或以上的長度就等於子節點被覆蓋了1次或以上的長度之和。
而CL[0]建樹時就賦值,之后不需要修改。
(3):如何維護掃描線過程中線段的數量
- struct Node{
- int cover;
- int lines;
- bool L,R;
- Node operator +(const Node &B){
- Node C;
- C.cover=0;
- C.lines=lines+B.lines-(R&&B.L);
- C.L=L;C.R=B.R;
- return C;
- }
- }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值。
所以周長就是以上兩部分的和。而兩部分怎么維護,前面都講過了,下面給出代碼。
- struct Node{
- int cover;
- int lines;
- bool L,R;
- int CoverLength;
- int Length;
- Node(){}
- Node(int cover,int lines,bool L,bool R,int CoverLength):cover(cover),lines(lines),L(L),R(R),CoverLength(CoverLength){}
- Node operator +(const Node &B){
- Node C;
- C.cover=0;
- C.lines=lines+B.lines-(R&&B.L);
- C.CoverLength=CoverLength+B.CoverLength;
- C.L=L;C.R=B.R;
- C.Length=Length+B.Length;
- return C;
- }
- }K[maxn<<2];
- void PushUp(int rt){
- if(K[rt].cover){
- K[rt].CoverLength=K[rt].Length;
- K[rt].L=K[rt].R=K[rt].lines=1;
- }
- else{
- K[rt]=K[rt<<1]+K[rt<<1|1];
- }
- }
掃描的代碼:
- int PreX=L[0].x;
- int ANS=0;
- int PreLength=0;
- int PreLines=0;
- Build(1,20001,1);
- for(int i=0;i<nL;++i){
-
- if(L[i].c) Cover(L[i].y1,L[i].y2-1,1,20001,1);
- else Uncover(L[i].y1,L[i].y2-1,1,20001,1);
-
- ANS+=2*PreLines*(L[i].x-PreX);
- PreLines=K[1].lines;
- PreX=L[i].x;
-
- ANS+=abs(K[1].CoverLength-PreLength);
- PreLength=K[1].CoverLength;
- }
- 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))的空間
下面是代碼:
- int L[maxnn],R[maxnn],Sum[maxnn],T[maxn],TP;
- void Add(int &rt,int l,int r,int x){
- ++TP;L[TP]=L[rt];R[TP]=R[rt];Sum[TP]=Sum[rt]+1;rt=TP;
- if(l==r) return;
- int m=(l+r)>>1;
- if(x <= m) Add(L[rt],l,m,x);
- else Add(R[rt],m+1,r,x);
- }
- int Search(int TL,int TR,int l,int r,int k){
- if(l==r) return l;
- int m=(l+r)>>1;
- if(Sum[L[TR]]-Sum[L[TL]]>=k) return Search(L[TL],L[TR],l,m,k);
- else return Search(R[TL],R[TR],m+1,r,k-Sum[L[TR]]+Sum[L[TL]]);
- }
以上就是主席樹部分的代碼。
熟悉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:判斷某個子串是否構成回文串。
題意:對一個集合進行插入與刪除操作。要求詢問某個時刻,集合中的元素從小到大排序之后,序號%5 ==3 的元素值之和。
這題其實不一定要用線段樹去做的,不過線段樹還是可以做的。
題意:有一個板,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