數據結構——線段樹
O、引例
A.給出n個數,n<=100,和m個詢問,每次詢問區間[l,r]的和,並輸出。
一種回答:這也太簡單了,O(n)枚舉搜索就行了。
另一種回答:還用得着o(n)枚舉,前綴和o(1)就搞定。
那好,我再修改一下題目。
B.給出n個數,n<=100,和m個操作,每個操作可能有兩種:1、在某個位置加上一個數;2、詢問區間[l,r]的和,並輸出。
回答:o(n)枚舉。
動態修改最起碼不能用靜態的前綴和做了。
好,我再修改題目:
C.給出n個數,n<=1000000,和m個操作,每個操作可能有兩種:1、在某個位置加上一個數;2、詢問區間[l,r]的和,並輸出。
回答:o(n)枚舉絕對超時。
再改:
D,給出n個數,n<=1000000,和m個操作,每個操作修改一段連續區間[a,b]的值
回答:從a枚舉到b,一個一個改。。。。。。有點兒常識的人都知道超時
那怎么辦?這就需要一種強大的數據結構:線段樹。
一、基本概念
1、線段樹是一棵二叉搜索樹,它儲存的是一個區間的信息。
2、每個節點以結構體的方式存儲,結構體包含以下幾個信息:
區間左端點、右端點;(這兩者必有)
這個區間要維護的信息(事實際情況而定,數目不等)。
3、線段樹的基本思想:二分。
4、線段樹一般結構如圖所示:
5、特殊性質:
由上圖可得,
1、每個節點的左孩子區間范圍為[l,mid],右孩子為[mid+1,r]
2、對於結點k,左孩子結點為2*k,右孩子為2*k+1,這符合完全二叉樹的性質
二、線段樹的基礎操作
注:以下基礎操作均以引例中的求和為例,結構體以此為例:
struct node
{
int l,r,w;//l,r分別表示區間左右端點,w表示區間和
}tree[4*n+1];
線段樹的基礎操作主要有5個:
建樹、單點查詢、單點修改、區間查詢、區間修改。
1、建樹,即建立一棵線段樹
① 主體思路:a、對於二分到的每一個結點,給它的左右端點確定范圍。
b、如果是葉子節點,存儲要維護的信息。
c、狀態合並。
②代碼
void build(int l,int r,int k) { tree[k].l=l;tree[k].r=r; if(l==r)//葉子節點 { scanf("%d",&tree[k].w); return ; } int m=(l+r)/2; build(l,m,k*2);//左孩子 build(m+1,r,k*2+1);//右孩子 tree[k].w=tree[k*2].w+tree[k*2+1].w;//狀態合並,此結點的w=兩個孩子的w之和 }
③注意
a.結構體要開4倍空間,為啥自己畫一個[1,10]的線段樹就懂了
b.千萬不要漏了return語句,因為到了葉子節點不需要再繼續遞歸了。
2、單點查詢,即查詢一個點的狀態,設待查詢點為x
①主體思路:與二分查詢法基本一致,如果當前枚舉的點左右端點相等,即葉子節點,就是目標節點。如果不是,因為這是二分法,所以設查詢位置為x,當前結點區間范圍為了l,r,中點為 mid,則如果x<=mid,則遞歸它的左孩子,否則遞歸它的右孩子
②代碼
void ask(int k) { if(tree[k].l==tree[k].r) //當前結點的左右端點相等,是葉子節點,是最終答案 { ans=tree[k].w; return ; } int m=(tree[k].l+tree[k].r)/2; if(x<=m) ask(k*2);//目標位置比中點靠左,就遞歸左孩子 else ask(k*2+1);//反之,遞歸右孩子 }
③正確性分析:
因為如果不是目標位置,由if—else語句對目標位置定位,逐步縮小目標范圍,最后一定能只到達目標葉子節點。
3、單點修改,即更改某一個點的狀態。用引例中的例子,對第x個數加上y
①主體思路
結合單點查詢的原理,找到x的位置;根據建樹狀態合並的原理,修改每個結點的狀態。
②代碼
void add(int k) { if(tree[k].l==tree[k].r)//找到目標位置 { tree[k].w+=y; return; } int m=(tree[k].l+tree[k].r)/2; if(x<=m) add(k*2); else add(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w;//所有包含結點k的結點狀態更新 }
4、區間查詢,即查詢一段區間的狀態,在引例中為查詢區間[x,y]的和
①主體思路
mid=(l+r)/2
y<=mid ,即 查詢區間全在,當前區間的左子區間,往左孩子走
x>mid 即 查詢區間全在,當前區間的右子區間,往右孩子走
否則,兩個子區間都走
②代碼
void sum(int k) { if(tree[k].l>=x&&tree[k].r<=y) { ans+=tree[k].w; return; } int m=(tree[k].l+tree[k].r)/2; if(x<=m) sum(k*2); if(y>m) sum(k*2+1); }
③正確性分析
情況1,3不用說,對於情況2,最差情況是搜到葉子節點,此時一定滿足情況1
5、區間修改,即修改一段連續區間的值,我們已給區間[a,b]的每個數都加x為例講解
Ⅰ.引子
有人可能就想到了:
修改的時候只修改對查詢有用的點。
對,這就是區間修改的關鍵思路。
為了實現這個,我們引入一個新的狀態——懶標記。
Ⅱ 懶標記
(懶標記比較難理解,我盡力講明白。。。。。。)
1、直觀理解:“懶”標記,懶嘛!用到它才動,不用它就睡覺。
2、作用:存儲到這個節點的修改信息,暫時不把修改信息傳到子節點。就像家長扣零花錢,你用的時候才給你,不用不給你。
3、實現思路(重點):
a.原結構體中增加新的變量,存儲這個懶標記。
b.遞歸到這個節點時,只更新這個節點的狀態,並把當前的更改值累積到標記中。注意是累積,可以這樣理解:過年,很多個親戚都給你壓歲錢,但你暫時不用,所以都被你父母扣下了。
c.什么時候才用到這個懶標記?當需要遞歸這個節點的子節點時,標記下傳給子節點。這里不必管用哪個子節點,兩個都傳下去。就像你如果還有妹妹,父母給你們零花錢時總不能偏心吧
d.下傳操作:
3部分:①當前節點的懶標記累積到子節點的懶標記中。
②修改子節點狀態。在引例中,就是原狀態+子節點區間點的個數*父節點傳下來的懶標記。
這就有疑問了,既然父節點都把標記傳下來了,為什么還要乘父節點的懶標記,乘自己的不行嗎?
因為自己的標記可能是父節點多次傳下來的累積,每次都乘自己的懶標記造成重復累積
③父節點懶標記清0。這個懶標記已經傳下去了,不清0后面再用這個懶標記時會重復下傳。就像你父母給了你5元錢,你不能說因為前幾次給了你10元錢, 所以這次給了你15元,那你不就虧大了。
懶標記下穿代碼:f為懶標記,其余變量與前面含義一致。
void down(int k) { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; }
Ⅲ 完整的區間修改代碼:
void add(int k) { if(tree[k].l>=a&&tree[k].r<=b)//當前區間全部對要修改的區間有用 { tree[k].w+=(tree[k].r-tree[k].l+1)*x;//(r-1)+1區間點的總數 tree[k].f+=x; return; } if(tree[k].f) down(k);//懶標記下傳。只有不滿足上面的if條件才執行,所以一定會用到當前節點的子節點 int m=(tree[k].l+tree[k].r)/2; if(a<=m) add(k*2); if(b>m) add(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w;//更改區間狀態 }
Ⅳ.懶標記的引入對其他基本操作的影響
因為引入了懶標記,很多用不着的更改狀態存了起來,這就會對區間查詢、單點查詢造成一定的影響。
所以在使用了懶標記的程序中,單點查詢、區間查詢也要像區間修改那樣,對用得到的懶標記下傳。其實就是加上一句if(tree[k].f) down(k),其余不變。
2017.5.16 之前寫的單點修改不需要下傳懶標記,在此訂正:單點修改也需要下傳懶標記
引入了懶標記的單點查詢代碼:
void ask(int k)//單點查詢 { if(tree[k].l==tree[k].r) { ans=tree[k].w; return ; } if(tree[k].f) down(k);//懶標記下傳,唯一需要更改的地方 int m=(tree[k].l+tree[k].r)/2; if(x<=m) ask(k*2); else ask(k*2+1); }
引入了懶標記的區間查詢代碼:
void sum(int k)//區間查詢 { if(tree[k].l>=x&&tree[k].r<=y) { ans+=tree[k].w; return; } if(tree[k].f) down(k)//懶標記下傳,唯一需要更改的地方 int m=(tree[k].l+tree[k].r)/2; if(x<=m) sum(k*2); if(y>m) sum(k*2+1); }
三、總結
線段樹5種基本操作代碼:
#include<cstdio> using namespace std; int n,p,a,b,m,x,y,ans; struct node { int l,r,w,f; }tree[400001]; inline void build(int k,int ll,int rr)//建樹 { tree[k].l=ll,tree[k].r=rr; if(tree[k].l==tree[k].r) { scanf("%d",&tree[k].w); return; } int m=(ll+rr)/2; build(k*2,ll,m); build(k*2+1,m+1,rr); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void down(int k)//標記下傳 { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; } inline void ask_point(int k)//單點查詢 { if(tree[k].l==tree[k].r) { ans=tree[k].w; return ; } if(tree[k].f) down(k); int m=(tree[k].l+tree[k].r)/2; if(x<=m) ask_point(k*2); else ask_point(k*2+1); } inline void change_point(int k)//單點修改 { if(tree[k].l==tree[k].r) { tree[k].w+=y; return; } if(tree[k].f) down(k); int m=(tree[k].l+tree[k].r)/2; if(x<=m) change_point(k*2); else change_point(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void ask_interval(int k)//區間查詢 { if(tree[k].l>=a&&tree[k].r<=b) { ans+=tree[k].w; return; } if(tree[k].f) down(k); int m=(tree[k].l+tree[k].r)/2; if(a<=m) ask_interval(k*2); if(b>m) ask_interval(k*2+1); } inline void change_interval(int k)//區間修改 { if(tree[k].l>=a&&tree[k].r<=b) { tree[k].w+=(tree[k].r-tree[k].l+1)*y; tree[k].f+=y; return; } if(tree[k].f) down(k); int m=(tree[k].l+tree[k].r)/2; if(a<=m) change_interval(k*2); if(b>m) change_interval(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } int main() { scanf("%d",&n);//n個節點 build(1,1,n);//建樹 scanf("%d",&m);//m種操作 for(int i=1;i<=m;i++) { scanf("%d",&p); ans=0; if(p==1) { scanf("%d",&x); ask_point(1);//單點查詢,輸出第x個數 printf("%d",ans); } else if(p==2) { scanf("%d%d",&x,&y); change_point(1);//單點修改 } else if(p==3) { scanf("%d%d",&a,&b);//區間查詢 ask_interval(1); printf("%d\n",ans); } else { scanf("%d%d%d",&a,&b,&y);//區間修改 change_interval(1); } } }
四、空間優化
父節點k,左二子2*k,右兒子2*k+1,需要4*n的空間
但並不是所有的葉子節點占用到2n+1——4n
這就造成大量空間浪費
2*n空間表示法:推薦博客:http://www.cppblog.com/MatoNo1/archive/2015/05/05/195857.html
用dfs序表示做節點下標
父節點k,左兒子k+1,右兒子:k+左兒子區間長度*2,不是父節點下標+父節點區間長度。因為當樹不滿時,兩者不相等
具體實現這里就不再寫模板了,就是改改左右兒子的下標
可參考代碼: 題目:樓房重建 http://www.cnblogs.com/TheRoadToTheGold/p/6361242.html
里面的建樹用的2*n空間
五、模板題
1、codevs 1080 線段樹練習 (單點修改+區間查詢) http://codevs.cn/problem/1080/

#include<cstdio> using namespace std; int n,m,p,x,y,ans; struct node { int l,r,w; }tree[400001]; inline void build(int l,int r,int k) { tree[k].l=l;tree[k].r=r; if(l==r) { scanf("%d",&tree[k].w); return ; } int m=(l+r)/2; build(l,m,k*2); build(m+1,r,k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void add(int k) { if(tree[k].l==tree[k].r) { tree[k].w+=y; return; } int m=(tree[k].l+tree[k].r)/2; if(x<=m) add(k*2); else add(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void sum(int k) { if(tree[k].l>=x&&tree[k].r<=y) { ans+=tree[k].w; return; } int m=(tree[k].l+tree[k].r)/2; if(x<=m) sum(k*2); if(y>m) sum(k*2+1); } int main() { scanf("%d",&n); build(1,n,1); scanf("%d",&m); for(int i=1;i<=m;i++) { scanf("%d%d%d",&p,&x,&y); ans=0; if(p==1) add(1); else { sum(1); printf("%d\n",ans); } } }
2、codevs 1081 線段樹練習2 (單點查詢+區間修改) http://codevs.cn/problem/1081/

#include<cstdio> using namespace std; int n,p,a,b,m,x,ans; struct node { int l,r,w,f; }tree[400001]; inline void build(int k,int ll,int rr) { tree[k].l=ll,tree[k].r=rr; if(tree[k].l==tree[k].r) { scanf("%d",&tree[k].w); return; } int m=(ll+rr)/2; build(k*2,ll,m); build(k*2+1,m+1,rr); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void down(int k) { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; } inline void add(int k) { if(tree[k].l>=a&&tree[k].r<=b) { tree[k].w+=(tree[k].r-tree[k].l+1)*x; tree[k].f+=x; return; } if(tree[k].f) down(k); int m=(tree[k].l+tree[k].r)/2; if(a<=m) add(k*2); if(b>m) add(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void ask(int k) { if(tree[k].l==tree[k].r) { ans=tree[k].w; return; } if(tree[k].f) down(k); int m=(tree[k].l+tree[k].r)/2; if(x<=m) ask(k*2); else ask(k*2+1); } int main() { scanf("%d",&n); build(1,1,n); scanf("%d",&m); for(int i=1;i<=m;i++) { scanf("%d",&p); if(p==1) { scanf("%d%d%d",&a,&b,&x); add(1); } else { scanf("%d",&x); ask(1); printf("%d\n",ans); } } }
3、codevs 1082 線段樹練習3 (區間修改+區間查詢)

#include<cstdio> using namespace std; int n,p,a,b,m,x,y; long long ans; struct node { long long l,r,w,f; }tree[800001]; inline void build(int k,int ll,int rr)//建樹 { tree[k].l=ll,tree[k].r=rr; if(tree[k].l==tree[k].r) { scanf("%d",&tree[k].w); return; } int m=(ll+rr)/2; build(k*2,ll,m); build(k*2+1,m+1,rr); tree[k].w=tree[k*2].w+tree[k*2+1].w; } inline void down(int k)//標記下穿 { tree[k*2].f+=tree[k].f; tree[k*2+1].f+=tree[k].f; tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1); tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1); tree[k].f=0; } inline void ask_interval(int k)//區間查詢 { if(tree[k].l>=a&&tree[k].r<=b) { ans+=tree[k].w; return; } if(tree[k].f) down(k); int m=(tree[k].l+tree[k].r)/2; if(a<=m) ask_interval(k*2); if(b>m) ask_interval(k*2+1); } inline void change_interval(int k)//區間修改 { if(tree[k].l>=a&&tree[k].r<=b) { tree[k].w+=(tree[k].r-tree[k].l+1)*y; tree[k].f+=y; return; } if(tree[k].f) down(k); int m=(tree[k].l+tree[k].r)/2; if(a<=m) change_interval(k*2); if(b>m) change_interval(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } int main() { scanf("%d",&n); build(1,1,n); scanf("%d",&m); for(int i=1;i<=m;i++) { scanf("%d",&p); ans=0; if(p==1) { scanf("%d%d%d",&a,&b,&y);//區間修改 change_interval(1); } else { scanf("%d%d",&a,&b);//區間查詢 ask_interval(1); printf("%lld\n",ans); } } }
六、經典例題
> codevs 3981/SPOJ GSS1/GSS3 ——區間最大子段和
> Bzoj3813 奇數國——區間內某個值是否出現過
>洛谷 P2894 酒店 Hotel ——區間連續一段空的長度
> codevs 2421 /Bzoj1858 序列操作——多種操作
> codevs 2000 / BZOJ 2957: 樓房重建——區間的最長上升子序列
Codevs3044 矩形面積求並——掃描線
代碼的話到隨筆分類——線段樹里找找吧 http://www.cnblogs.com/TheRoadToTheGold/category/933602.html