線段樹詳解 (原理,實現與應用)
一:綜述
二:原理
(1)線段樹的點修改:
(2)線段樹的區間查詢:
(3)線段樹的區間修改:
(4)線段樹的儲存結構:
三:遞迴實現
(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];//Sum求和,Add為懶惰標記
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];//儲存陣列值
return;
}
int m=(l+r)>>1;
//左右遞迴
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
//更新資訊
PushUp(rt);
}
(2)點修改:
void Update(int L,int C,int l,int r,int 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);//子節點更新了,所以本節點也需要更新資訊
}
(3)區間修改:
void Update(int L,int R,int C,int l,int r,int rt){//L,R表示操作區間,l,r表示當前節點區間,rt表示當前節點編號
if(L <= l && r <= R){//如果本區間完全在操作區間[L,R]以內
Sum[rt]+=C*(r-l+1);//更新數字和,向上保持正確
Add[rt]+=C;//增加Add標記,表示本區間的Sum正確,子區間的Sum仍需要根據Add的值來調整
return ;
}
int m=(l+r)>>1;
PushDown(rt,m-l+1,r-m);//下推標記
//這裡判斷左右子樹跟[L,R]有無交集,有交集才遞迴
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)區間查詢:
void PushDown(int rt,int ln,int rn){
//ln,rn為左子樹,右子樹的數字數量。
if(Add[rt]){
//下推標記
Add[rt<<1]+=Add[rt];
Add[rt<<1|1]+=Add[rt];
//修改子節點的Sum使之與對應的Add相對應
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){//L,R表示操作區間,l,r表示當前節點區間,rt表示當前節點編號
if(L <= l && r <= R){
//在區間內,直接返回
return Sum[rt];
}
int m=(l+r)>>1;
//下推標記,否則Sum可能不正確
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);
四:非遞迴原理
點修改:
點修改下的區間查詢:
區間修改下的區間查詢:
區間修改:
五:非遞迴實現
(0)定義:
//
#define maxn 100007
int A[maxn],n,N;//原陣列,n為原陣列元素個數 ,N為擴充元素個數
int Sum[maxn<<2];//區間和
int Add[maxn<<2];//懶惰標記
(1)建樹:
//
void Build(int n){
//計算N的值
N=1;while(N < n+2) N <<= 1;
//更新葉節點
for(int i=1;i<=n;++i) Sum[N+i]=A[i];//原陣列下標+N=儲存下標
//更新非葉節點
for(int i=N-1;i>0;--i){
//更新所有非葉節點的統計資訊
Sum[i]=Sum[i<<1]+Sum[i<<1|1];
//清空所有非葉節點的Add標記
Add[i]=0;
}
}
(2)點修改:
//
void Update(int L,int C){
for(int s=N+L;s;s>>=1){
Sum[s]+=C;
}
}
(3)點修改下的區間查詢:
//
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)區間修改:
<span style="font-size:14px;">//
void Update(int L,int R,int C){
int s,t,Ln=0,Rn=0,x=1;
//Ln: s一路走來已經包含了幾個數
//Rn: t一路走來已經包含了幾個數
//x: 本層每個節點包含幾個數
for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){
//更新Sum
Sum[s]+=C*Ln;
Sum[t]+=C*Rn;
//處理Add
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;
}
//更新上層Sum
for(;s;s>>=1,t>>=1){
Sum[s]+=C*Ln;
Sum[t]+=C*Rn;
}
} </span>
(5)區間修改下的區間查詢:
//
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):字串雜湊
//
#define K 137
#define maxn 100001
char str[maxn];
int Pow[maxn];//K的各個次方
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 題解//
#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){//更新rt節點的四個資料 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):計數排序
給定一個長度不超過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)總結:
七:掃描線
掃描線求重疊矩形面積:
//
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,前一個橫座標。
//
int PreL=0;//前一個L值,剛開始是0,所以第一次計算時不會引入誤差
int X;//X值
int ANS=0;//存累計面積
int I=0;//線段的下標
while(I < Ln){
//先計算面積
ANS+=PreL*(Line[I].x-X);
X=Line[I].x;//更新X值
//對所有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):線段樹中點的含義
//
int Rank[maxn],Rn;
void SetRank(){//呼叫前,所有y值被無序存入Rank陣列,下標為[1..Rn]
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;
//此時,所有y值被從小到大無重複地存入Rank陣列,下標為[1..Rn]
}
int GetRank(int x){//給定x,求x的下標
//二分法求下標
int L=1,R=Rn,M;//[L,R] first >=x
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] )。
//
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值到離散之後的下標的轉換。(2):如何維護覆蓋線段長度
//
struct Node{
int Cover;//區間整體被覆蓋的次數
int L;//Length : 所代表的區間總長度
int CL;//Cover Length :實際覆蓋長度
Node operator +(const Node &B)const{
Node X;
X.Cover=0;//因為若上級的Cover不為0,不會呼叫子區間加法函式
X.L=L+B.L;
X.CL=CL+B.CL;
return X;
}
}K[maxn<<2];
這樣定義之後,區間的資訊更新是這樣的:
//
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;//Cover
int CL[4];//CoverLength[0~3]
//CL[i]表示被覆蓋了大於等於i次的線段長度,CL[0]其實就是線段總長
}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().
(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];
要維護被分成多少個線段,就需要記錄左右端點是否被覆蓋,知道了這個,就可以合併區間了。
掃描線求重疊矩形周長:
//
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;//前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次或以上的體積:
八:可持久化 (主席樹)
//主席樹
int L[maxnn],R[maxnn],Sum[maxnn],T[maxn],TP;//左右子樹,總和,樹根,指標
void Add(int &rt,int l,int r,int x){//建立新樹,l,r是區間, 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){//區間查詢第k大
if(l==r) return l;//返回第k大的下標
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]]);
}
以上就是主席樹部分的程式碼。
九:練習題
適合非遞迴線段樹的題目:
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)),還是勉強過了這題。
掃描線題目:
遞迴線段樹題目:
給定一個長度不超過10^5的字串(小寫英文字母),和不超過5000個操作。
每個操作 L R K 表示給區間[L,R]的字串排序,K=1為升序,K=0為降序。
最後輸出最終的字串。
題意:有一個板,h行,每行w長度的位置。每次往上面貼一張海報,長度為1*wi .
每次貼的時候,需要找到最上面的,可以容納的空間,並且靠邊貼。
題意就是,給定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)+線段樹區間修改,不過是單邊界的區間,寫起來挺有趣。
另一種解法就是暴力,很巧妙的方法,高複雜度+低常數居然就這麼給過了。
樹套樹題目:
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
相關文章
- 線段樹(超詳解)
- 線段樹差分及其應用
- 簡單的線段樹應用
- 深入瞭解Rabbit加密技術:原理、實現與應用加密
- zkw 線段樹-原理及其擴充套件套件
- 熱修復(一)原理與實現詳解
- CF19D 線段樹+STL各種應用
- HTML程式碼混淆技術:原理、應用和實現方法詳解HTML
- 線~段~樹
- 線段樹
- 線段樹也能是 Trie 樹 題解
- 詳解布隆過濾器原理與實現過濾器
- Redis Sentinel實現的機制與原理詳解Redis
- 快速生成樹原理詳解
- 從原理到應用,Elasticsearch詳解Elasticsearch
- IOS SDWebImage實現原理詳解iOSWeb
- Spring AOP 實現原理與 CGLIB 應用SpringCGLib
- iOS–KVO的實現原理與具體應用iOS
- 區間演算法題用線段樹可以秒解?演算法
- 線段樹 hate it
- 【模版】線段樹
- 01 線段樹
- 線段樹--RMQMQ
- 李超線段樹
- 線段樹模板
- 多圖詳解萬星 Restful 框架原理與實現REST框架
- 線段樹分治略解&雜題解析
- quartz (從原理到應用)詳解篇quartz
- 詳解Spring Retry實現原理Spring
- Java HashMap 的實現原理詳解JavaHashMap
- ut.cpp 最大線段並減線段交 [線段樹]
- Java 讀寫鎖 ReadWriteLock 原理與應用場景詳解Java
- 詳解數倉物件設計中序列SEQUENCE原理與應用物件
- 工大培訓——day5 C題 線段樹變形應用
- 詳解JavaScript陣列特性與實踐應用JavaScript陣列
- 深入詳解Java反射機制與底層實現原理?Java反射
- 線段樹筆記筆記
- 線段樹入門