線段樹,強大的數據結構,用處也是比較廣的。
首先,我們要明白線段樹是個啥?
線段樹,線段嘛,有左右端點,那么它當然可以代表一個區間,那么區間上的好多事情都可以用它來搞,比如:區間加,區間乘,區間求和。
首先讓我們先看個線段樹的模型。
如圖,這就是一棵線段樹的模型。
圈內的點表示這是第幾個點,紅色表示這個點表示的區間范圍。
每個點和它的左右兩個兒子的編號是有一定的關系的:
點N,它的左兒子編號為N$\times$2,右兒子編號為N$\times$2+1.
線段樹支持單點修改,區間修改,單點查詢,區間查詢。
講解有易到難。
先放一張后邊當例子講解的圖(每個圈中的數表示的為這個區間的和)。
構建線段樹框架
假設一段長度為 N 的序列,那么我們需要維護總長為 1--N 的線段。
對於每一個點,我們需要確定它所表示的線段的 左端點 右端點 以及我們要維護的區間和
對於每個點的左兒子和右兒子來說,左兒子繼承前一半 [L,(L+R)/2],右兒子繼承后一半( (L+R)/2,R ]。
還有我們維護的區間和,每個大區間都是有兩個小區間組成,那么 大區間的和 = 左兒子的和+右兒子的和。
這部分代碼:
struct ahah{ long long l,r,sum,f; //對於 f 的作用,后邊會有解釋,此處忽略。 }tree[200000<<2]; 注意此處四倍空間。 void build(int k,int l,int r) { tree[k].l=l;tree[k].r=r; if(tree[k].l==tree[k].r) { scanf("%lld",&tree[k].sum); return ; } long long mid=(tree[k].l+tree[k].r)>>1; build(k<<1,l,mid); build(k<<1|1,mid+1,r); tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; }
單點查詢與修改
單點修改,我們已知單點的位置,那么我們從一號點開始,根據兩個兒子所代表的區間范圍,選擇下一步是走左兒子還是右兒子,今兒一步步的確定准確的點。
單點查詢與單點修改幾乎一樣,查詢到具體的位置后,輸出其結果。
拿上邊的圖進行模擬下:
修改4號點:左兒子[0,4],右兒子[5,8] ->選擇左兒子 ->左兒子[0,2],右兒子[3,4] ->選擇右兒子 ->... -> 找到4號點修改。
查詢同上。
當我們修改完某個點以后,包含這個點的區間的和發生了改變,所以最后我們還要加一句:
$tree[k].sum=tree[k \times 2].sum+tree[k \times 2+1].sum$ 以確保維護的區間和不會改變。
代碼:k表示點的編號,需要給x號點加上y 。
void update(int k) { if(tree[k].l==tree[k].r) { tree[k].sum+=y; return ; } long long mid=(tree[k].l+tree[k].r)>>1; if(x<=mid)update(k<<1); else update(k<<1|1); tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; }
區間求和與修改
區間修改與查詢也有很大的相似。
區間修改,暫時來說我們沒有好辦法,只能一個一個的修改區間中的每一個元素,后邊會有優秀做法的講解。
區間查詢,我們需要明確這個被查詢的區間位置。
以下被查詢的區間用[a,b]表示,k表示當前的點的編號。
首先我們從最大的區間開始,判斷被查詢的區間,有三種情況:
1.位於左兒子中($b \le tree[k<<1].l $)還是右兒子中($a > tree[k<<1|1].l $),然后選擇下一步是去左兒子還是右兒子。
2.被查詢的區間被兩部分都包括,那么我們就將區間分開,一部分查詢左區間,一部分查詢右區間。
3.現在的點所代表的區間$(a <= tree[k].l , b >= tree[k].r )$ 被要查詢的區間所包含,那么不需要再查下去,直接將答案加上這段區間所維護的和就好了。
拿查詢區間[3,5]模擬一下:
mid表示當前區間的二分點。
代碼用遞歸實現:
void query(int k) { if(x<=tree[k].l&&y>=tree[k].r) { ans+=tree[k].sum; return ; } if(tree[k].f)down(k); //先省略就好。 long long mid=(tree[k].l+tree[k].r)>>1; if(x<=mid)query(k<<1); if(y>mid)query(k<<1|1); }
重點來了:懶標記
對於區間修改來說,我們一個一個的修改浪費大量的時間,並且修改了還不一定查修這個點,為了解決這個問題,我們引入懶標記 f 。
首先我們要明確他的一個性質: 懶,用得着的時候動一下,用不着的時候就永遠在那。
每個節點的的懶標記記錄的是它所代表的這個區間所加的值 f 。
就像區間查詢一樣,當區間不被包含時,分開查找,當目前區間已被要修改的區間包含時,那么我們就可以直接給這個點,打上懶標記,不需要去准確的一個一個的修改區間內的元素了。
那這樣的話必究沒法維護區間和了?
我們維護區間和為的是啥?當然是為了求區間和了,當我們在查詢的時候,若用得到這整個區間,那么返回 維護的值 + 區間元素個數$\times$懶標記的值,若不全用得到的話,那么我們將懶標記下傳給它的左右兩個兒子,然后繼續查找。區間和並不是沒有維護,而是在維護懶標記從而間接地維護者區間和。
這里需要注意的是:當節點的懶標記下傳給兒子的時候它的懶標記則需要清空,因為已經傳給了兒子。
懶標記下傳代碼:
void down(long long k) { tree[k<<1].f+=tree[k].f; tree[k<<1|1].f+=tree[k].f; tree[k<<1].sum+=(tree[k<<1].r-tree[k<<1].l+1)*tree[k].f; tree[k<<1|1].sum+=(tree[k<<1|1].r-tree[k<<1|1].l+1)*tree[k].f; tree[k].f=0; }
用到懶標記的區間加以及求和:
void query(int k) { if(x<=tree[k].l&&y>=tree[k].r) { ans+=tree[k].sum; return ; } if(tree[k].f)down(k); long long mid=(tree[k].l+tree[k].r)>>1; if(x<=mid)query(k<<1); if(y>mid)query(k<<1|1); } void add(long long k) { if(tree[k].l>=x&&tree[k].r<=y) { tree[k].sum+=(tree[k].r-tree[k].l+1)*val; tree[k].f+=val; return ; } if(tree[k].f) down(k); long long mid=(tree[k].l+tree[k].r)>>1; if(x<=mid)add(k<<1); if(y>mid)add(k<<1|1); tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; }
綜上就是先對簡單的線段樹操作。
貼上模板:
#include<cstdio> #include<iostream> using namespace std; long long n,m,ans,x,y,ch,val; struct ahah{ long long l,r,sum,f; }tree[200000<<2]; void build(int k,int l,int r) { tree[k].l=l;tree[k].r=r; if(tree[k].l==tree[k].r) { scanf("%lld",&tree[k].sum); return ; } long long mid=(tree[k].l+tree[k].r)>>1; build(k<<1,l,mid); build(k<<1|1,mid+1,r); tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; } void update(int k) { if(tree[k].l==tree[k].r) { tree[k].sum+=y; return ; } long long mid=(tree[k].l+tree[k].r)>>1; if(x<=mid)update(k<<1); else update(k<<1|1); tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; } void down(long long k) { tree[k<<1].f+=tree[k].f; tree[k<<1|1].f+=tree[k].f; tree[k<<1].sum+=(tree[k<<1].r-tree[k<<1].l+1)*tree[k].f; tree[k<<1|1].sum+=(tree[k<<1|1].r-tree[k<<1|1].l+1)*tree[k].f; tree[k].f=0; } void query(int k) { if(x<=tree[k].l&&y>=tree[k].r) { ans+=tree[k].sum; return ; } if(tree[k].f)down(k); long long mid=(tree[k].l+tree[k].r)>>1; if(x<=mid)query(k<<1); if(y>mid)query(k<<1|1); } void add(long long k) { if(tree[k].l>=x&&tree[k].r<=y) { tree[k].sum+=(tree[k].r-tree[k].l+1)*val; tree[k].f+=val; return ; } if(tree[k].f) down(k); long long mid=(tree[k].l+tree[k].r)>>1; if(x<=mid)add(k<<1); if(y>mid)add(k<<1|1); tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; } int main() { scanf("%lld%lld",&n,&m); build(1,1,n); for(int i=1;i<=m;i++) { ans=0; cin>>ch>>x>>y; if(ch==1) { cin>>val; add(1); } else { query(1); cout<<ans<<"\n"; } } }
例題:
入門
模板:洛谷線段樹1:https://www.luogu.org/problemnew/show/P3372
單點修改與區間查詢:最大數https://www.luogu.org/problemnew/show/P1198
進階:
妖夢斬木棒:https://www.luogu.org/problemnew/show/P3797
無聊的數列:https://www.luogu.org/problemnew/show/P1438