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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

(转)线段树的区间更新

發(fā)布時(shí)間:2025/5/22 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 (转)线段树的区间更新 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

原文地址:http://blog.csdn.net/zip_fan/article/details/46775633

寫的很好,昨天剛剛開始寫線段樹,有些地方還不是很明白,看了這篇博文,學(xué)會(huì)了數(shù)組形式保存線段樹,還學(xué)會(huì)了區(qū)間更新

以下為轉(zhuǎn)載的博文內(nèi)容

?

距離第一次接觸線段樹已經(jīng)一年多了,再次參加ACM暑假集訓(xùn),這一次輪到我們這些老家伙們給學(xué)弟學(xué)妹們講解線段樹了,所以就自己重新把自己做過的題目看了一遍,然后寫篇博客紀(jì)念一下。作為一個(gè)菜鳥,文中肯定有很多表達(dá)不是很準(zhǔn)確甚至錯(cuò)誤的地方,歡迎各位大牛指正。

? ? ? ? 作為近年來算法競(jìng)賽里面最火爆的數(shù)據(jù)結(jié)構(gòu)考點(diǎn),它的用法和問法層出不窮。而作為解決反復(fù)對(duì)區(qū)間的更新和查詢問題最好的數(shù)據(jù)結(jié)構(gòu),它擁有其他數(shù)據(jù)結(jié)構(gòu)無法取代的地位。樹狀數(shù)組雖然也能解決很多問題,而且代碼量較低,空間復(fù)雜度較低,但是局限性較大,比如區(qū)間最值的問題就不能用樹狀數(shù)組完美解決。

? ? ? ? 那么接下來就為大家?guī)砭€段樹的兩種基本用法:單點(diǎn)更新和區(qū)間更新(又叫成段更新)。

? ? ? ? 首先線段樹它是一棵高度平衡的二叉樹,那么很多二叉樹的性質(zhì)它是完美繼承的,比如用數(shù)組去模擬的話,父節(jié)點(diǎn)的下標(biāo)*2=左兒子的下標(biāo),父節(jié)點(diǎn)的下標(biāo)*2+1=右兒子的下標(biāo)。而這個(gè)性質(zhì)也在線段樹的實(shí)現(xiàn)中得到了很好的運(yùn)用(當(dāng)然線段樹還有很多不同的寫法,大家可以自行研究,這里不做展開)。

? ? ? ? 那么什么是線段樹呢?

? ? ? ? 我們來看一道題:

HDU1166敵兵布陣

? ? ? ? 這道題如果用常規(guī)暴力的做法,就把所有營(yíng)地的士兵存在一個(gè)數(shù)組里面,然后對(duì)于每次操作直接更新對(duì)應(yīng)位置的數(shù),對(duì)于每次詢問直接從i到j(luò)加起來。然而這么操作下來,對(duì)于極限數(shù)據(jù)50000個(gè)人,40000條命令,顯然是會(huì)超時(shí)的,那么一種新的數(shù)據(jù)結(jié)構(gòu)線段樹就應(yīng)運(yùn)而生了。

? ? ? ? 首先第一個(gè)疑問:為什么線段樹會(huì)快?

? ? ? ? 顯然對(duì)于m個(gè)點(diǎn)n次詢問,暴力的做法時(shí)間復(fù)雜度是O(m*n)的。然而線段樹作為一棵二叉樹,繼承了二叉樹O(logn)的優(yōu)良品質(zhì),對(duì)于這道題最壞的復(fù)雜度也是O(m*logn)的,這個(gè)量顯然是符合時(shí)間要求的。

? ? ? ? 第二:線段樹如何處理?

? ? ? ? 倘若節(jié)點(diǎn)x(x為奇數(shù))記錄的是第1個(gè)點(diǎn)的數(shù)據(jù),節(jié)點(diǎn)x+1記錄的是第2個(gè)點(diǎn)的數(shù)據(jù),那么節(jié)點(diǎn)x/2記錄的就是區(qū)間[1,2]上的有效數(shù)據(jù),以此類推,最頂端的父節(jié)點(diǎn)記錄的就是區(qū)間[1,n]上的有效數(shù)據(jù),那么對(duì)于每個(gè)點(diǎn)的數(shù)據(jù),有且僅有l(wèi)ogn個(gè)節(jié)點(diǎn)的數(shù)據(jù)會(huì)被它影響,因此每次更新只用更新logn個(gè)點(diǎn),查詢亦然,這樣就有效地節(jié)約了時(shí)間。

? ? ? ? 對(duì)于每個(gè)節(jié)點(diǎn),其代表的是區(qū)間[x,y]之間的值,那么其左兒子節(jié)點(diǎn)代表的就是[x,(x+y)/2]區(qū)間的值,右兒子節(jié)點(diǎn)代表的是區(qū)間[(x+y)/2+1,y]上的值,既保證了無重復(fù),又保證了樹的層數(shù)最短,查詢效率最高。

? ? ? ? 第三:線段樹的具體實(shí)現(xiàn)呢?

? ? ? ? 那么我們就跟著剛才拿到題目來詳細(xì)講解。

int tre[N*4]; void build(int num,int le,int ri) { if(le==ri) { scanf("%d",&tre[num]); return ; } int mid=(le+ri)/2; build(num*2,le,mid); build(num*2+1,mid+1,ri); tre[num]=tre[num*2]+tre[num*2+1]; }

?首先是建樹,在這里num存的是下標(biāo),而le和ri表示的是這個(gè)區(qū)間的左右端點(diǎn),那么每往下一層num*2,區(qū)間則折半,保證了最少的層數(shù),而此時(shí)內(nèi)存占用大約為4倍的點(diǎn)數(shù),所以開數(shù)組的時(shí)候開tre[4*N]。這個(gè)題因?yàn)樾枰x入每個(gè)點(diǎn),作為二叉樹的先序遍歷,很好地保證了第x個(gè)點(diǎn)正好讀入在le=ri=x的那個(gè)tre[num]里面。而父親節(jié)點(diǎn)所代表的區(qū)間包含了子節(jié)點(diǎn)所代表的區(qū)間,所以子節(jié)點(diǎn)的值又會(huì)影響父節(jié)點(diǎn),因此每次建立完兒子節(jié)點(diǎn)之后,又會(huì)通過tre[num]=tre[num*2]+tre[num*2+1];操作將父親節(jié)點(diǎn)初始化,當(dāng)然此處為求和操作所以是+,不同的題可以選擇取最值等不同運(yùn)算符。當(dāng)然不同的題根據(jù)需求可以采取對(duì)tre[num]賦值或者memset等方法來建樹以及初始化。

void update(int num,int le,int ri,int x,int y) { if(le==ri) { tre[num]+=y; return ; } int mid=(le+ri)/2; if(x<=mid) update(num*2,le,mid,x,y); else update(num*2+1,mid+1,ri,x,y); tre[num]=tre[num*2]+tre[num*2+1]; }

接下來是修改操作,繼承了上面的num,le,ri,保證了一致性,同時(shí)此處做的是對(duì)于第x個(gè)點(diǎn)增加y個(gè)人的操作,所以尋找到x所對(duì)應(yīng)的tre[num],然后操作,并回退。而此時(shí)需要注意的是,對(duì)于x操作了之后,所有包含x的區(qū)間的tre[num]都需要被修改,因此也就有了在回退前的tre[num]=tre[num*2]+tre[num*2+1];操作。而這個(gè)題操作的是增加減少(減少直接傳-x),而其他的諸如取最大最小值、取異或值等等都只用對(duì)于對(duì)應(yīng)的運(yùn)算符做修改即可。

int query(int num,int le,int ri,int x,int y) { if(x<=le&&y>=ri) { return tre[num]; } int mid=(le+ri)/2; int ans=0; if(x<=mid) ans+=query(num*2,le,mid,x,y); if(y>mid) ans+=query(num*2+1,mid+1,ri,x,y); return ans; }

最后是查詢操作,依然繼承了num,le,ri。而此處做的是區(qū)間查詢,(其實(shí)如果x=y就成了單點(diǎn)查詢)那么如果查詢區(qū)間[x,y]包含了目前的區(qū)間[le,ri],即x<=le&&y>=ri,那么此時(shí)的tre[num]就已經(jīng)是這一部分的有效數(shù)據(jù)了,所以直接return即可,否則繼續(xù)分區(qū)間查詢。同樣,此時(shí)根據(jù)題意所做的求和操作可以對(duì)應(yīng)替換為異或、取最值等操作。

?

? ? ? ? 以上就是線段樹最簡(jiǎn)單的功能——單點(diǎn)更新。

? ? ? ? 下面為大家?guī)淼木€段樹稍微難一點(diǎn)但是基本是最常用的一個(gè)用法:區(qū)間更新。

區(qū)間更新對(duì)于初學(xué)者來說是一個(gè)坎,其中有幾步相對(duì)較難理解。但是只要掌握,就能解決絕大多數(shù)線段樹的題目了。

? ? ? ? 首先剛剛那個(gè)題每次是每個(gè)營(yíng)地增減人,那么如果每次是x號(hào)營(yíng)地到y(tǒng)號(hào)營(yíng)地每次都增減人呢?這樣我們就會(huì)發(fā)現(xiàn)單點(diǎn)更新操作不適用了,無論我們?nèi)绾握{(diào)整都無法達(dá)到效果,而且即使每次對(duì)于x到y(tǒng)之間每個(gè)營(yíng)地都執(zhí)行一次單點(diǎn)操作,結(jié)果上看似可以,但是極限情況下我們每次對(duì)于1到n號(hào)進(jìn)行更新的話,復(fù)雜度就會(huì)達(dá)到O(m*n*logn),這樣就絕對(duì)會(huì)超時(shí)了,那么怎么做呢?這里就要用到區(qū)間更新了。

? ? ? ? 對(duì)于區(qū)間更新的,我們來看下面這個(gè)題:

HDU1556Color the ball

? ? ? ? 這個(gè)題很顯然滿足剛剛所提到的每次對(duì)于其中的一段里面的所有點(diǎn)進(jìn)行操作。

? ? ? ? 那么既然是線段樹,首先我們依然是建樹初始化,在建樹階段區(qū)別不多,該讀值的讀值,該賦值的賦值,該置0的置0,這題根據(jù)需要,在最開始所有的球都是未被涂色的,那么直接所有tre[num]置為0即可,于是這一次我們就可以不必單獨(dú)寫一個(gè)build了,直接memset(tre,0,sizeof(tre));即可。

? ? ? ? 但是與單點(diǎn)更新最大的不同就是:它多了一個(gè)lazy數(shù)組!!!!!!!!!!重要的地方要打10個(gè)感嘆號(hào)。

? ? ? ? laz,全稱lazy,中文叫懶惰標(biāo)記或者延遲更新標(biāo)記。

? ? ? ? 因?yàn)槲覀冎?#xff0c;如果我們每次都把段更新到節(jié)點(diǎn)上,那么操作次數(shù)和每次對(duì)區(qū)間里面的每個(gè)點(diǎn)單點(diǎn)更新是完全一樣的哇!那么怎么辦呢?仔細(xì)觀察線段樹,你會(huì)發(fā)現(xiàn)一個(gè)非常神奇的地方:每個(gè)節(jié)點(diǎn)表示的值都是區(qū)間[le,ri]之間的值有木有!!!!!!!!!!為什么說它神奇呢?更新的區(qū)間的值,存的區(qū)間的值!簡(jiǎn)直就是天作之合,我每次更新到對(duì)應(yīng)區(qū)間了我就放著,我等下次需要往下更新更小的區(qū)間的時(shí)候,再把兩次的值一起更新下去有木有啊!可以節(jié)約非常多時(shí)間啊有木有啊!

? ? ? ?對(duì),這就是laz[num]的作用。下面我們跟著題再來逐步感受。

? ? ? ?首先在最最最最最最開始,是沒有進(jìn)行過更新操作的,那么laz[num]自然是全部置為0(當(dāng)然有的題有額外的初始化要求,大家根據(jù)題目自行定奪)。

? ? ? ?那么初始化結(jié)束之后,就開始更新操作。

void update(int num,int le,int ri,int x,int y) { if(x<=le&&y>=ri) { tre[num]++; laz[num]++; return ; } pushdown(num); int mid=(le+ri)/2; if(x<=mid) update(num*2,le,mid,x,y); if(y>mid) update(num*2+1,mid+1,ri,x,y); }

這一段是對(duì)區(qū)間[x,y]上的每個(gè)氣球都涂色一次,當(dāng)然你也可以涂z次色,無非就是額外再傳一個(gè)變量代表涂色次數(shù)嘛。然后依然是分區(qū)間查找,一直到目標(biāo)區(qū)間[x,y]包含了當(dāng)前區(qū)間[le,ri],那么就對(duì)tre[num]和laz[num]操作,代表對(duì)這個(gè)區(qū)間我這么修改,而這個(gè)區(qū)間的分區(qū)間也應(yīng)該被做修改,但是這時(shí)候我為了節(jié)約時(shí)間,暫時(shí)不做修改,但是我把這個(gè)修改動(dòng)作存在laz[num]里,下次需要的時(shí)候再修改。

?

? ? ? ?而下次是什么時(shí)候呢?就是當(dāng)前區(qū)間比我需要的目標(biāo)區(qū)間大的時(shí)候,我必須用到下面的值了,那么就迫不得已了,不能再懶惰下去了,必須往下修改了,這時(shí)候,我們就把之前堆積起來的懶惰標(biāo)記pushdown了,于是就有了一個(gè)神奇的pushdown操作。

void pushdown(int num) { if(laz[num]!=0) { tre[num*2]+=laz[num]; tre[num*2+1]+=laz[num]; laz[num*2]+=laz[num]; laz[num*2+1]+=laz[num]; laz[num]=0; } }

?以上就是這個(gè)題的pushdown操作。當(dāng)laz[num]!=0時(shí),也就是這個(gè)點(diǎn)存在懶惰標(biāo)記的時(shí)候,我們就要往下更新了,然而這個(gè)題是否判斷l(xiāng)az[num]的有無對(duì)整體影響不大,但是有部分題再pushdown的同時(shí)會(huì)對(duì)整體有影響,例如取最小值的時(shí)候?qū)τ谝欢瓮瑫r(shí)置為一個(gè)數(shù),那么如果不判斷0,就會(huì)把0給誤pushdown下去,這就必須判斷l(xiāng)az[num]的有無了。

?

? ? ? ? 那么laz[num]本來是應(yīng)該修改下去的,所以會(huì)對(duì)兩個(gè)兒子節(jié)點(diǎn)的tre[num]有影響,這里因?yàn)槭乔蠹?#xff0c;所以采用的是+號(hào),其它操作根據(jù)具體題目替換。同時(shí),兒子節(jié)點(diǎn)的兒子節(jié)點(diǎn)也應(yīng)該被更新,但是我們依然懶,趕一步走一步,所以此時(shí)依然不更新兒子節(jié)點(diǎn)的兒子節(jié)點(diǎn),而是用更新兒子節(jié)點(diǎn)的laz標(biāo)記來代替。

? ? ? ? 回到之前的updata操作,在每次修改了兒子節(jié)點(diǎn)的tre[num]之后,正常情況下應(yīng)該對(duì)父親節(jié)點(diǎn)的tre[num]進(jìn)行修改,但是此題父親節(jié)點(diǎn)的tre[num]不會(huì)對(duì)后面的結(jié)果造成影響,所以這里就偷懶省略了這一步操作,實(shí)際操作中絕大部分題目都是不可以省略的,必須在更新完兒子節(jié)點(diǎn)的值之后再反過來更新父親節(jié)點(diǎn)。

? ? ? ? 最后依然是query操作。

int query(int num,int le,int ri,int x) { if(le==ri) { return tre[num]; } pushdown(num); int mid=(le+ri)/2; if(x<=mid) return query(num*2,le,mid,x); else return query(num*2+1,mid+1,ri,x); }

與單點(diǎn)更新時(shí)的query非常相似(廢話吼,線段樹本來就是一個(gè)比較模板的東西),也是額外加了一個(gè)pushdown操作,原因與update的一樣,最后也缺省了反過來對(duì)父親節(jié)點(diǎn)的更新,原因同上,不再贅述。

?

?

? ? ? ? 至此線段樹的兩種基本用法:單點(diǎn)更新和區(qū)間更新操作就已經(jīng)介紹完畢了。相信大家如果能仔細(xì)看完的話,對(duì)于這個(gè)數(shù)據(jù)結(jié)構(gòu)也應(yīng)該有了一定的認(rèn)識(shí)。而線段樹還有掃描線、區(qū)間合并等高級(jí)用法,而且線段樹作為一個(gè)數(shù)據(jù)結(jié)構(gòu),是必然會(huì)和其它算法發(fā)生化學(xué)反應(yīng)的,例如矩陣、dp等操作都有可能被巧妙地嵌套在線段樹上形成一個(gè)綜合題,所以大家下去一定要多做題,多感悟,才能深入透徹地吃下這個(gè)知識(shí)點(diǎn)。

?

?

<---------------------------------------------------------------------------------------------------------------------------------------------------------->

void update(int i,int k,int val,int q){//單個(gè)點(diǎn)的更新if( A[i].lt == k && A[i].rt == k ){if( q ){A[i].val += val;}else{A[i].val -= val;}return ;}if( k <= A[i<<1].rt ){update( i<<1,k,val,q );//自己體會(huì)}if( k>= A[i<<1|1].lt ){update( i<<1|1,k,val,q );}A[i].val = (A[i<<1].val + A[i<<1|1].val ); }int query(int i,int lt,int rt){if( A[i].lt >= lt && A[i].rt <= rt ){return A[i].val;}int a = 0;int b = 0;if( lt <= A[i<<1].rt ){//左區(qū)間有涉及就向左查詢a = query( i<<1,lt,rt );}if( rt >= A[i<<1|1].lt ){//右區(qū)間有涉及就向右查詢b = query( i<<1|1,lt,rt );}return (a+b); }

?

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

總結(jié)

以上是生活随笔為你收集整理的(转)线段树的区间更新的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

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