淺談線段樹原理及實現
大家好,給大家介紹完了樹狀數組(有興趣的讀者可以在我的博客文章中閱讀),現在來給大家介紹另一種數據結構——線段樹。它們結構都有共同點,但是線段樹更為復雜,功能也更為強大,接下來就會一步一步向你介紹線段樹的功能和用法。
線段樹(Segment Tree)的簡介:
線段樹是一種二叉搜索樹,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點,它基於分之思想,用於在線性區間上完成動態統計,它的思想主要是將線性的區間遞歸劃分成長度相同的兩段,直到葉子節點,每個葉子表示一個數據,向上每一層父親節點都涵蓋了所有兒子節點,模型圖如下:
在圖上,我們可以看出,它的根節點即為我們需要修改和統計的線段,它將其劃分成兩段子線段,運用了二分的思想mid=(l+r)/2,我們再來想想看一個節點應該包含什麽數據,首先肯定是它的範圍L和R,在就是一個數據域date,至於這個date存儲什麽數據,那就根據需要而定,例如可以存儲這一段的最值,也可存儲這一段的和等等,當然也可以都存儲,無非就是多幾個變量,基本的節點信息就是這些,然後再考慮一下用多大的數組來存儲,根據上圖,我們可以發現由於是二分的思想,所以不一定是理想的滿二叉樹,一個節點可能會有空的子節點,N個節點的滿二叉樹節點為N+N/2+N/4+...+1=2*N-1,然後再下面一層節點數為2N,要空余出來,所以存儲1~N數組要開4*N,可以用一下代碼表示:
struct Segment_tree{ int l,r; int date; }node[SIZE*4];
由此我們可以總結一下線段樹節點的特點:
- 線段樹每個節點代表著一個區間。
- 線段樹的根節點表示統計範圍區間1~n。
- 線段樹每個葉子節點代表著一個數值。
- 對於每個除葉子節點外的節點[L,R],它的左子節點[L,mid],右子節點[mid+1,R]。
- 對於編號為i的節點,左子節點編號2*i,右子節點編號2*i+1。
由此我們可以遞歸建線段樹,代碼如下:
void Build_SegmentTree(int p,int L,int R) { node[p].tag=0;node[p].sum=0; node[p].l=L,node[p].r=R; if(L==R){node[p].sum=num[L];return;} int mid=(L+R)/2; Build_SegmentTree(2*p,L,mid); Build_SegmentTree(2*p+1,mid+1,R); node[p].sum=node[2*p].sum+node[2*p+1].sum; }
線段樹經典例題與分析:
已知一個數列,你需要進行下面兩種操作:
1.將某區間每一個數加上x
2.求出某區間每一個數的和
輸入格式:
第一行包含兩個整數N、M,分別表示該數列數字的個數和操作的總個數。
第二行包含N個用空格分隔的整數,其中第i個數字表示數列第i項的初始值。
接下來M行每行包含3或4個整數,表示一個指令,具體如下:
1: 格式:1 x y k 含義:將區間[x,y]內每個數加上k
2: 格式:2 x y 含義:輸出區間[x,y]內每個數的和
輸出格式:
輸出包含若幹行整數,即為所有指令2的結果。
這道題是一個典型的線段樹例題,當然同樣可以用樹狀數組來做,用一個循環來修改[x,y]的葉子節點值,再用容斥原理來求出[x,y]的和,但考慮這樣做法的時間復雜度T(n)=O((y-x)logn),並不能滿足100000的數據規模,所以來考慮線段樹。在這道題中,考慮修改節點後改變的量,改變的是所有父親節點,那麽我們這裏節點參數要帶一個叫做“懶惰標記tag”的參數,用來表示我們對這個節點之下葉子節點的修改量,再用一個sum來表示葉子節點對父親節點的增加量,線段樹節點以及建樹過程代碼如下:
struct Segment_tree { int l,r; long long sum,tag; }node[SIZE*4]; void Build_SegmentTree(int p,int L,int R) { node[p].tag=0;node[p].sum=0; node[p].l=L,node[p].r=R; if(L==R){node[p].sum=num[L];return;} int mid=(L+R)/2; Build_SegmentTree(2*p,L,mid); Build_SegmentTree(2*p+1,mid+1,R); node[p].sum=node[2*p].sum+node[2*p+1].sum; }
接下來思考對節點的修改,對於修改線段[L,R],如果包含了當前節點的全部,那麽給這個節點打上懶惰標記,加上這個節點所有的葉子數leave*變化量k,再遞歸的分別向左子節點和右子節點修改值,代碼如下:
void Segment_update(int L,int R,int k,int rt) { if(L<=node[rt].l&&R>=node[rt].r) { node[rt].tag+=k; node[rt].sum+=(long long)k*(node[rt].r-node[rt].l+1); return; } pushdown(rt); int mid=(node[rt].l+node[rt].r)/2; if(L<=mid) Segment_update(L,R,k,rt*2); if(R>mid) Segment_update(L,R,k,rt*2+1); node[rt].sum=node[rt*2].sum+node[rt*2+1].sum; }
讀者可能發現了代碼中有一個未知的函數pushdown(),這個函數便代表著懶惰標記下移,這下我們知道了為什麽叫它懶惰標記了,因為如果不需要他,就沒有必要一直遞歸到葉子節點來修改節點值,而是在父親節點上標上對葉子節點的修改量,就是所謂的懶惰標記,當區間涉及到子節點線段時才下移懶惰標記來計算對於這一子段和的修改量,是不是很巧妙?pushdown函數代碼如下:
void pushdown(int p) { if(node[p].tag!=0) { node[p*2].sum+=node[p].tag*(node[p*2].r-node[p*2].l+1); node[p*2+1].sum+=node[p].tag*(node[p*2+1].r-node[p*2+1].l+1); node[p*2].tag+=node[p].tag; node[p*2+1].tag+=node[p].tag; node[p].tag=0; } return; }
接下來就很簡單了,遞歸的詢問線段值,不多介紹,代碼如下:
long long Segment_Query(int p,int L,int R) { if(L<=node[p].l&&R>=node[p].r) return node[p].sum; pushdown(p); int mid=(node[p].l+node[p].r)/2; long long ans=0; if(L<=mid) ans+=Segment_Query(p*2,L,R); if(R>mid) ans+=Segment_Query(p*2+1,L,R); return ans; }
再組織一下題意寫出完整程序如下:
#include <bits/stdc++.h> #define SIZE 100005 using namespace std; int N,Q,num[SIZE]; struct Segment_tree { int l,r; long long sum,tag; }node[SIZE*4]; void Build_SegmentTree(int p,int L,int R) { node[p].tag=0;node[p].sum=0; node[p].l=L,node[p].r=R; if(L==R){node[p].sum=num[L];return;} int mid=(L+R)/2; Build_SegmentTree(2*p,L,mid); Build_SegmentTree(2*p+1,mid+1,R); node[p].sum=node[2*p].sum+node[2*p+1].sum; } void pushdown(int p) { if(node[p].tag!=0) { node[p*2].sum+=node[p].tag*(node[p*2].r-node[p*2].l+1); node[p*2+1].sum+=node[p].tag*(node[p*2+1].r-node[p*2+1].l+1); node[p*2].tag+=node[p].tag; node[p*2+1].tag+=node[p].tag; node[p].tag=0; } return; } void Segment_update(int L,int R,int k,int rt) { if(L<=node[rt].l&&R>=node[rt].r) { node[rt].tag+=k; node[rt].sum+=(long long)k*(node[rt].r-node[rt].l+1); return; } pushdown(rt); int mid=(node[rt].l+node[rt].r)/2; if(L<=mid) Segment_update(L,R,k,rt*2); if(R>mid) Segment_update(L,R,k,rt*2+1); node[rt].sum=node[rt*2].sum+node[rt*2+1].sum; } long long Segment_Query(int p,int L,int R) { if(L<=node[p].l&&R>=node[p].r) return node[p].sum; pushdown(p); int mid=(node[p].l+node[p].r)/2; long long ans=0; if(L<=mid) ans+=Segment_Query(p*2,L,R); if(R>mid) ans+=Segment_Query(p*2+1,L,R); return ans; } int main() { cin>>N>>Q; for(int i=1;i<=N;i++) cin>>num[i]; Build_SegmentTree(1,1,N); for(int i=1;i<=Q;i++) { int code,par1,par2,par3; scanf("%d",&code); if(code==1){scanf("%d%d%d",&par1,&par2,&par3);Segment_update(par1,par2,par3,1);} if(code==2){scanf("%d%d",&par1,&par2);printf("%lld\n",Segment_Query(1,par1,par2));} } return 0; }
淺談線段樹原理及實現