線段樹入門(分塊講解)
在一些題目涉及到區間修改和區間求和的情況,如果我們每次修改與求和的時間復雜度均為O(n)在大數據的情況下是會超時的,因此我們要引進一個維護一個區間的數據結構——線段樹.
[算法描述(線段樹)]
線段樹顧名思義就是由線段組成的樹,我們知道線段有兩個端點中間有一條直線,在線段樹中我們可以把每一個節點內都看成一個線段,每個節點都維護個左端點l和右端點r那么該節點所維護的區間就是(l,r),每一個節點再維護一個相對於這個區間的數值例如和或最大值或最小值.
[代碼(線段樹)]
struct Segment_Tree{ //線段樹(結構體形式) long long sum; //這個線段樹維護的數值(可以是和也可以是最大值,最小值) long long lazy; //懶標記(后面講) }tree[maxm<<2]; //maxm為這個線段樹的大小一般要開4倍
[算法描述(建樹)]
我們就給予這個樹一個定義,我們把所有線段樹的根節點均設為1,它維護的區間是1到n(n為維護的數據總數),每一個父節點(包括根節點,假定它維護區間為(l,r))維護的左節點的區間是(l,(l+r)>>1),右節點維護的區間是(((l+r)>>1)+1,r),因此我們得知線段樹是一個二叉樹,我們最后在設立一個邊界就是當一個節點(l,r)l=r時它是這個線段樹的一個葉子節點,就不繼續遞歸建樹了.在寫函數之前我們先給建樹等所有函數一個定義,由於我們知道根節點維護的區間,我們可以動態地求出每一個節點的區間(因為只和父節點有關且當父節點被訪問到后才可能訪問到子節點)我們便不把每個節點維護的區間記錄在每個節點上了,因此后面的函數的變量可能有很多,我們給予每一個函數一個定義變量的順序(若不需要某些變量則上整,當然變量可能會更多這時另行判斷),第一個變量為當前更新的節點,第二個變量為當前節點維護的區間左端,第三個變量為當前節點維護的區間右端,第四個變量為需要修改的區間左端,第五個變量為需要修改的區間右端,第六個變量為需要修改的相應數值,我們將這些函數定下一個定義變量的規律便於我們記憶這些函數.
[代碼(LC/RC)]
# define ll long long # define LC (x<<1) # define RC (x<<1|1)
其中 我們將每一個父節點的左節點定義為x<<1;右節點定義為(x<<1)+1及x<<1|1
[代碼(push_up)]
inline void push_up(ll x){ //這個函數是用其子節點更新父節點的相應數值(可以是和,最大值,最小值甚至矩陣等等) tree[x].sum=tree[LC].sum+tree[RC].sum; //我們可以發現父節點維護的區間是兩個子節點維護的區間之和(無交叉) return; }
[代碼(build)]
void build(ll x,ll l,ll r){ //建樹,x為當前建立的節點,其維護的區間為(l,r) if(l==r){ //l==r時這個節點記錄的是葉子節點 tree[x].sum=a[l]; //直接賦值(a[l]為原先的序列) return; } int mid=(l+r)>>1; //取中間值並下取整,將其設為左子樹和右子樹的分界線 build(LC,l,mid); //遞歸建立左子樹其維護區間為(l,mid) build(RC,mid+1,r); //遞歸建立右子樹其維護區間為(mid+1,r)這里+1是因為使區間不重疊 push_up(x); //把子樹信息更新到x節點上 return; }
[算法描述(區間修改,查詢)]
我們現在來說線段樹的一個重要操作——區間修改,假設我們需要修改區間(l,r),如果我們維護相應的葉子節點都修改一遍這個時間復雜度最壞的情況下是O(n log n)的,這樣線段樹也就沒有優勢了.於是我們引入了變量—懶標記(lazy),顧名思義懶標記懶標記,就是我們懶得修改從而打的標記,我們如何懶且能使答案正確呢,我們知道我們只需要修改區間(l,r),我們需要修改的點維護的區間一定在需要修改的區間內,因此如果一個節點再需要修改的區間內,我們可以直接修改一個大區間相對應的值,並不需要修改它的子節點,等我們需要單獨提出該子節點的信息的時候在下傳這個懶標記並修改這個子節點,這樣我們可以在O(log n)的情況下完成一整個區間的修改了,對於查找區間相似的我們只需要查找完全在修改區間內的數值即可.
[代碼(free)]
inline void free(ll x,ll l,ll r,ll k){ //修改節點x->(l,r)的懶標記修改值為k tree[x].lazy+=k; //更新懶標記數值 tree[x].sum+=k*(r-l+1); //區間修改數值 return; }
[代碼(push_down)]
inline void push_down(ll x,ll l,ll r){ //下傳懶標記(需要用到x的子樹) ll mid=(l+r)>>1; //x為需要修改的節點,(l,r)為所維護的區間 free(LC,l,mid,tree[x].lazy); //向左子樹傳遞懶標記 free(RC,mid+1,r,tree[x].lazy); //向右子樹傳遞懶標記 tree[x].lazy=0; //情況節點x的懶標記 return; }
[代碼(update)]
inline void update(ll x,ll l,ll r,ll q_l,ll q_r,ll k){ //節點x維護區間(l,r),需要修改的區間是q_l,q_r,修改相應數值為k if(q_l<=l && r<=q_r){ //如果這個節點維護的區間(l,r)完全在所需要修改的區間內 free(x,l,r,k); //先修改該區間的lazy並更新相應數值 return; } push_down(x,l,r); //這個區間維護的區間不在需要區間內(順便下傳懶標記) ll mid=(l+r)>>1; if(q_l<=mid){ update(LC,l,mid,q_l,q_r,k); //如果左子樹中有節點維護的區間在修改區間內 } if(q_r>mid){ update(RC,mid+1,r,q_l,q_r,k); //如果右子樹中有節點維護的區間在修改區間內 } push_up(x); //修改后當然要更新一下節點x return; }
[代碼(區間查詢)]
inline ll query(ll x,ll l,ll r,ll q_l,ll q_r){ ll res=0; //當前節點為x,維護區間(l,r)查詢區間(q_l,q_r) //res這個根節點所能找到的區間 if(q_l<=l && r<=q_r){ //如果這個區間完全在查找區間內直接返回數值 return tree[x].sum; } ll mid=(l+r)>>1; push_down(x,l,r); //順便釋放一下這個點的懶標記 if(q_l<=mid){ res+=query(LC,l,mid,q_l,q_r); //向左節點索取答案 } if(q_r>mid){ res+=query(RC,mid+1,r,q_l,q_r); //向右節點更新答案 } return res; //返回答案 }
[代碼(洛谷P3372)]
/* Name: Segment Tree Author: FZSZ-LinHua Date: 2018 06 11 Time complexity: ??? Algorithm: Segment Tree */ # include "cstdio" # include "iostream" # define ll long long # define LC (x<<1) # define RC (x<<1|1) using namespace std; const int maxm=100000+10; ll n,m,a[maxm]; struct Segment_Tree{ //線段樹(結構體形式) long long sum; //這個線段樹維護的數值(可以是和也可以是最大值,最小值) long long lazy; //懶標記(后面講) }tree[maxm<<2]; //maxm為這個線段樹的大小一般要開4倍 inline void push_up(ll x){ //這個函數是用其子節點更新父節點的相應數值(可以是和,最大值,最小值甚至矩陣等等) tree[x].sum=tree[LC].sum+tree[RC].sum; //我們可以發現父節點維護的區間是兩個子節點維護的區間之和(無交叉) return; } void build(ll x,ll l,ll r){ //建樹,x為當前建立的節點,其維護的區間為(l,r) if(l==r){ //l==r時這個節點記錄的是葉子節點 tree[x].sum=a[l]; //直接賦值(a[l]為原先的序列) return; } int mid=(l+r)>>1; //取中間值並下取整,將其設為左子樹和右子樹的分界線 build(LC,l,mid); //遞歸建立左子樹其維護區間為(l,mid) build(RC,mid+1,r); //遞歸建立右子樹其維護區間為(mid+1,r)這里+1是因為使區間不重疊 push_up(x); //把子樹信息更新到x節點上 return; } inline void free(ll x,ll l,ll r,ll k){ //修改節點x->(l,r)的懶標記修改值為k tree[x].lazy+=k; //更新懶標記數值 tree[x].sum+=k*(r-l+1); //區間修改數值 return; } inline void push_down(ll x,ll l,ll r){ //下傳懶標記(需要用到x的子樹) ll mid=(l+r)>>1; //x為需要修改的節點,(l,r)為所維護的區間 free(LC,l,mid,tree[x].lazy); //向左子樹傳遞懶標記 free(RC,mid+1,r,tree[x].lazy); //向右子樹傳遞懶標記 tree[x].lazy=0; //情況節點x的懶標記 return; } inline void update(ll x,ll l,ll r,ll q_l,ll q_r,ll k){ //節點x維護區間(l,r),需要修改的區間是q_l,q_r,修改相應數值為k if(q_l<=l && r<=q_r){ //如果這個節點維護的區間(l,r)完全在所需要修改的區間內 free(x,l,r,k); //先修改該區間的lazy並更新相應數值 return; } push_down(x,l,r); //這個區間維護的區間不在需要區間內(順便下傳懶標記) ll mid=(l+r)>>1; if(q_l<=mid){ update(LC,l,mid,q_l,q_r,k); //如果左子樹中有節點維護的區間在修改區間內 } if(q_r>mid){ update(RC,mid+1,r,q_l,q_r,k); //如果右子樹中有節點維護的區間在修改區間內 } push_up(x); //修改后當然要更新一下節點x return; } inline ll query(ll x,ll l,ll r,ll q_l,ll q_r){ ll res=0; if(q_l<=l && r<=q_r){ return tree[x].sum; } ll mid=(l+r)>>1; push_down(x,l,r); if(q_l<=mid){ res+=query(LC,l,mid,q_l,q_r); } if(q_r>mid){ res+=query(RC,mid+1,r,q_l,q_r); } return res; } int main(){ int x,y,c,k; scanf("%lld%lld",&n,&m); register int i; for(i=1;i<=n;i++){ scanf("%lld",&a[i]); } build(1,1,n); for(i=1;i<=m;i++){ scanf("%d",&c); if(c==1){ scanf("%d%d%d",&x,&y,&k); update(1,1,n,x,y,k); }else{ scanf("%d%d",&x,&y); printf("%lld\n",query(1,1,n,x,y)); } } return 0; }