日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

(转载)浅谈线段树

發(fā)布時間:2023/12/2 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 (转载)浅谈线段树 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

淺談線段樹

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?????數(shù)據(jù)結(jié)構(gòu)——線段樹

O、引例

A.給出n個數(shù),n<=100,和m個詢問,每次詢問區(qū)間[l,r]的和,并輸出。

一種回答:這也太簡單了,O(n)枚舉搜索就行了。

另一種回答:還用得著o(n)枚舉,前綴和o(1)就搞定。

那好,我再修改一下題目。

B.給出n個數(shù),n<=100,和m個操作,每個操作可能有兩種:1、在某個位置加上一個數(shù);2、詢問區(qū)間[l,r]的和,并輸出。

回答:o(n)枚舉。

動態(tài)修改最起碼不能用靜態(tài)的前綴和做了。

好,我再修改題目:

C.給出n個數(shù),n<=1000000,和m個操作,每個操作可能有兩種:1、在某個位置加上一個數(shù);2、詢問區(qū)間[l,r]的和,并輸出。

回答:o(n)枚舉絕對超時。

再改:

D,給出n個數(shù),n<=1000000,和m個操作,每個操作修改一段連續(xù)區(qū)間[a,b]的值

回答:從a枚舉到b,一個一個改。。。。。。有點兒常識的人都知道超時

那怎么辦?這就需要一種強大的數(shù)據(jù)結(jié)構(gòu):線段樹。

一、基本概念

1、線段樹是一棵二叉搜索樹,它儲存的是一個區(qū)間的信息。

2、每個節(jié)點以結(jié)構(gòu)體的方式存儲,結(jié)構(gòu)體包含以下幾個信息:

? ? ?區(qū)間左端點、右端點;(這兩者必有)

? ? ?這個區(qū)間要維護的信息(事實際情況而定,數(shù)目不等)。

3、線段樹的基本思想:二分

4、線段樹一般結(jié)構(gòu)如圖所示:

5、特殊性質(zhì):

由上圖可得,

1、每個節(jié)點的左孩子區(qū)間范圍為[l,mid],右孩子為[mid+1,r]

2、對于結(jié)點k,左孩子結(jié)點為2*k,右孩子為2*k+1,這符合完全二叉樹的性質(zhì)

二、線段樹的基礎(chǔ)操作

注:以下基礎(chǔ)操作均以引例中的求和為例,結(jié)構(gòu)體以此為例:

struct node
{
? ? ? ?int l,r,w;//l,r分別表示區(qū)間左右端點,w表示區(qū)間和
}tree[4*n+1];

線段樹的基礎(chǔ)操作主要有5個:

建樹、單點查詢、單點修改、區(qū)間查詢、區(qū)間修改。

1、建樹,即建立一棵線段樹

? ?① 主體思路:a、對于二分到的每一個結(jié)點,給它的左右端點確定范圍。

? ? ? ? ? ? ? ? ? ? ?b、如果是葉子節(jié)點,存儲要維護的信息。

? ? ? ? ? ? ? ? ? ? ?c、狀態(tài)合并。

? ②代碼

void build(int l,int r,int k) {tree[k].l=l;tree[k].r=r;if(l==r)//葉子節(jié)點 {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;//狀態(tài)合并,此結(jié)點的w=兩個孩子的w之和 }

③注意

?a.結(jié)構(gòu)體要開4倍空間,為啥自己畫一個[1,10]的線段樹就懂了

?b.千萬不要漏了return語句,因為到了葉子節(jié)點不需要再繼續(xù)遞歸了。

2、單點查詢,即查詢一個點的狀態(tài),設(shè)待查詢點為x

? ?①主體思路:與二分查詢法基本一致,如果當前枚舉的點左右端點相等,即葉子節(jié)點,就是目標節(jié)點。如果不是,因為這是二分法,所以設(shè)查詢位置為x,當前結(jié)點區(qū)間范圍為了l,r,中點為 ? ? ? ? mid,則如果x<=mid,則遞歸它的左孩子,否則遞歸它的右孩子

? ?②代碼

void ask(int k) {if(tree[k].l==tree[k].r) //當前結(jié)點的左右端點相等,是葉子節(jié)點,是最終答案 {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語句對目標位置定位,逐步縮小目標范圍,最后一定能只到達目標葉子節(jié)點。

3、單點修改,即更改某一個點的狀態(tài)。用引例中的例子,對第x個數(shù)加上y

①主體思路

?結(jié)合單點查詢的原理,找到x的位置;根據(jù)建樹狀態(tài)合并的原理,修改每個結(jié)點的狀態(tài)。

?②代碼

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;//所有包含結(jié)點k的結(jié)點狀態(tài)更新 }

4、區(qū)間查詢,即查詢一段區(qū)間的狀態(tài),在引例中為查詢區(qū)間[x,y]的和

①主體思路

?

?

mid=(l+r)/2

y<=mid ,即 查詢區(qū)間全在,當前區(qū)間的左子區(qū)間,往左孩子走

x>mid?即 查詢區(qū)間全在,當前區(qū)間的右子區(qū)間,往右孩子走

否則,兩個子區(qū)間都走

②代碼

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,最差情況是搜到葉子節(jié)點,此時一定滿足情況1

5、區(qū)間修改,即修改一段連續(xù)區(qū)間的值,我們已給區(qū)間[a,b]的每個數(shù)都加x為例講解

? ? Ⅰ.引子

?

? ? ? ?有人可能就想到了:

? ? ? ?修改的時候只修改對查詢有用的點。

? ? ? ?對,這就是區(qū)間修改的關(guān)鍵思路。

? ? ? 為了實現(xiàn)這個,我們引入一個新的狀態(tài)——懶標記

? Ⅱ 懶標記

? ? ?(懶標記比較難理解,我盡力講明白。。。。。。)

??? ? ?1、直觀理解:“懶”標記,懶嘛!用到它才動,不用它就睡覺。

? ? ? ?2、作用:存儲到這個節(jié)點的修改信息,暫時不把修改信息傳到子節(jié)點。就像家長扣零花錢,你用的時候才給你,不用不給你。

? ? ? ?3、實現(xiàn)思路(重點):

? ? ? ? ???a.原結(jié)構(gòu)體中增加新的變量,存儲這個懶標記。

? ? ? ? ???b.遞歸到這個節(jié)點時,只更新這個節(jié)點的狀態(tài),并把當前的更改值累積到標記中。注意是累積,可以這樣理解:過年,很多個親戚都給你壓歲錢,但你暫時不用,所以都被你父母扣下了。

? ? ? ? ? ?c.什么時候才用到這個懶標記?當需要遞歸這個節(jié)點的子節(jié)點時,標記下傳給子節(jié)點。這里不必管用哪個子節(jié)點,兩個都傳下去。就像你如果還有妹妹,父母給你們零花錢時總不能偏心吧

? ? ? ? ? ?d.下傳操作:

? ? ? ? ? ? ? ?3部分:①當前節(jié)點的懶標記累積到子節(jié)點的懶標記中。

? ? ? ? ? ? ? ? ? ? ? ? ?②修改子節(jié)點狀態(tài)。在引例中,就是原狀態(tài)+子節(jié)點區(qū)間點的個數(shù)*父節(jié)點傳下來的懶標記

? ? ? ? ? ? ? ? ? ? ? ? ? ? 這就有疑問了,既然父節(jié)點都把標記傳下來了,為什么還要乘父節(jié)點的懶標記,乘自己的不行嗎?

? ? ? ? ? ? ? ? ? ? ? ? ? ? 因為自己的標記可能是父節(jié)點多次傳下來的累積,每次都乘自己的懶標記造成重復(fù)累積

? ? ? ? ? ? ? ? ? ? ? ? ?③父節(jié)點懶標記清0。這個懶標記已經(jīng)傳下去了,不清0后面再用這個懶標記時會重復(fù)下傳。就像你父母給了你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; }

?Ⅲ?完整的區(qū)間修改代碼:

void add(int k) {if(tree[k].l>=a&&tree[k].r<=b)//當前區(qū)間全部對要修改的區(qū)間有用 {tree[k].w+=(tree[k].r-tree[k].l+1)*x;//(r-1)+1區(qū)間點的總數(shù)tree[k].f+=x;return;}if(tree[k].f) down(k);//懶標記下傳。只有不滿足上面的if條件才執(zhí)行,所以一定會用到當前節(jié)點的子節(jié)點 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;//更改區(qū)間狀態(tài) }

?Ⅳ.懶標記的引入對其他基本操作的影響

? ???因為引入了懶標記,很多用不著的更改狀態(tài)存了起來,這就會對區(qū)間查詢、單點查詢造成一定的影響。

? ? ?所以在使用了懶標記的程序中,單點查詢、區(qū)間查詢也要像區(qū)間修改那樣,對用得到的懶標記下傳。其實就是加上一句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); }

? ? 引入了懶標記的區(qū)間查詢代碼:

void sum(int k)//區(qū)間查詢 {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); }

三、總結(jié)

線段樹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)//區(qū)間查詢 {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)//區(qū)間修改 {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個節(jié)點 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個數(shù) printf("%d",ans);} else if(p==2){scanf("%d%d",&x,&y);change_point(1);//單點修改 }else if(p==3){scanf("%d%d",&a,&b);//區(qū)間查詢 ask_interval(1);printf("%d\n",ans);}else{scanf("%d%d%d",&a,&b,&y);//區(qū)間修改 change_interval(1);}} }

?

?四、空間優(yōu)化

父節(jié)點k,左二子2*k,右兒子2*k+1,需要4*n的空間

但并不是所有的葉子節(jié)點占用到2n+1——4n

這就造成大量空間浪費

2*n空間表示法:推薦博客:http://www.cppblog.com/MatoNo1/archive/2015/05/05/195857.html

用dfs序表示做節(jié)點下標

父節(jié)點k,左兒子k+1,右兒子:k+左兒子區(qū)間長度*2,不是父節(jié)點下標+父節(jié)點區(qū)間長度。因為當樹不滿時,兩者不相等

具體實現(xiàn)這里就不再寫模板了,就是改改左右兒子的下標

可參考代碼:?題目:樓房重建http://www.cnblogs.com/TheRoadToTheGold/p/6361242.html?

?

里面的建樹用的2*n空間

五、模板題

1、codevs 1080 線段樹練習(xí) (單點修改+區(qū)間查詢)? http://codevs.cn/problem/1080/??

?View Code

2、codevs 1081 線段樹練習(xí)2 (單點查詢+區(qū)間修改)?http://codevs.cn/problem/1081/

?View Code

3、codevs 1082 線段樹練習(xí)3 ?(區(qū)間修改+區(qū)間查詢)

?View Code

六、經(jīng)典例題

> codevs 3981/SPOJ GSS1/GSS3 ——區(qū)間最大子段和
> Bzoj3813 奇數(shù)國——區(qū)間內(nèi)某個值是否出現(xiàn)過
>洛谷 P2894 酒店 Hotel ——區(qū)間連續(xù)一段空的長度
> codevs 2421 /Bzoj1858 序列操作——多種操作
> codevs 2000 / BZOJ 2957: 樓房重建——區(qū)間的最長上升子序列
?Codevs3044 矩形面積求并——掃描線

?

作者:xxy 出處:http://www.cnblogs.com/TheRoadToTheGold/ 本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。

轉(zhuǎn)載于:https://www.cnblogs.com/rmy020718/p/8832889.html

總結(jié)

以上是生活随笔為你收集整理的(转载)浅谈线段树的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。