樹狀數組區間更新
樹狀數組區間更新
在今天的文章開始之前,給大家提一個建議,由於線段樹和樹狀數組這兩個結構的分析有很多聯系,因此,建議沒有看前幾篇文章的朋友一定需要了解一下前面的內容。鏈接如下:
線段樹+RMQ問題第二彈
線段樹第二彈(區間更新)
樹狀數組(Binary Indexed Tree,BIT)
上篇文章我們討論了樹狀數組的基本結構以及它最擅長的兩個功能:單點更新和區間求和,今天,我們來接著上一篇文章的內容繼續深入研究。上篇文章我們是將樹狀數組和線段樹進行對比講解的,既然線段樹的章節我們介紹了區間求和、區間最值、單點更新、區間更新,那麽對應的,樹狀數組也應該有個區間更新吧。但我們從上篇文章中的分析可以看出來,樹狀數組確實不擅長區間更新,它也無法進行區間更新,這是不是就意味著文章寫到這裏就結束了呢?當然不是,作為一個求真務實從不坑蒙拐騙的誠信公眾號,我當然不會做出那種用標題騙閱讀量的事情。那之前說的樹狀數組不會、不能進行區間更新是怎麽回事?答案就是樹狀數組可以利用某些手段(比如單點更新)來達到區間更新的目的,讓結果和區間更新後的結果相吻合,看似實現了區間更新,實際是偽區間更新。所以,坑蒙拐騙的是它,可不是我。沒有看過上篇文章的建議先看上篇文章(樹狀數組(Binary Indexed Tree,BIT))了解基礎知識,便於更好的理解本篇文章。
既然它是可以實現偽區間更新的,我們做題的時候又只是追求輸出結果的正確,不妨就花幾分鐘看一看到底它是怎麽實現的?
在此我們稱事先給定的數組為原數據數組,按照樹狀數組結構實現的數組稱為樹狀數組,本篇文章中討論的區間更新方式是給原數據數組一段區間的所有元素加上一個數值 a 。
從上篇文章中我們可以知道,樹狀數組的區間求和實際是通過區間兩端點的前綴和相減實現的,檢驗區間更新是否正確的方式便是區間求和,既然區間求和只需要保證區間兩端點的正確性,就給我們留出了可乘之機,我們可以在進行區間更新的時候只對兩端點進行更新操作,保證數值正確即可。現在,我們就需要對更新後整個樹狀數組的特征進行觀察,找出規律。
假設某次給原數據數組區間 i ~j 上的所有元素均加 a,此時若要查詢某一區間 p~q 的元素和,會是怎樣的結果呢?我們首先應該分別求解 p 和 q 位置的前綴和,p 和 q 求前綴和的方法是一樣的,所以我們研究的問題變成了區間更新和單點查詢,那麽一般的我們假設求 x 的前綴和,如下圖所示:
上圖中, i~j 表示區間更新的範圍,x 表示待求前綴和的結束元素位置, sum[x]表示區間更新前 x 位置的前綴和,a 表示區間更新過程中對每個元素增加的值,縱坐標 y 表示區間更新後相應的前綴和的增加量,即 y(x)+sum[x] 的值為區間更新後x位置的前綴和。由圖可知,
在 x<i 時,y 的值恒為 0
在i <= x <= j 時,y的值隨著 x 的增加而遞增
在 x > j 時,y的值恒為 (j - i +1) * a
我們知道,在單點更新的時候,對一個位置 x 的值加 a 時,位置在 x 之前的所有前綴和都是沒有影響的,受影響的是從 x 開始之後的所有位置的前綴和。
知道了這些之後,我們就可以利用上篇文章中實現的單點更新和區間查詢進行區間更新和單點查詢了,由上圖可知,在 x < i 和 x > j 的範圍內(待更新區間之外),只要區間更新的加數 a 確定,這些前綴和便是固定值,不需要知道單點查詢時候的位置,因此這部分工作可以在更新的時候實現,具體如何實現呢?
觀察上圖,我們發現 在i <= x <= j 時,前綴和增量 y = (x-i+1)*a = x*a -(i-1)*a ;在 x > j 時,前綴和增量 y = (j - i +1 )*a = j*a - (i-1)*a ; 由此可以得出結論,在 x >= i 時,y 的表達式中均包含 -(i-1)*a ,所以根據前綴和的規律,我們利用單點更新將此值加在 i 位置的前綴和上,則可以達到給 x >= i 範圍內的所有前綴和均加上了此值。在 i <= x <= j 的區間範圍內,我們發現前綴和增量表達式 y 除 -(i-1)*a 這部分外,剩余的部分為 x*a ,x 即為單點查詢的位置,這個值不確定,只有在某一次查詢的時候才能被確定,因此,這個操作需要在單點查詢的時候完成。在 x > j 的範圍內,前綴和表達式 y = j*a - (i-1)*a , 除已經在 x = i 的位置處理的 -(i-1)*a 這部分之外,剩余部分為 j*a ,這部分值只與區間更新的端點和更新的加數 a 有關,因此可以在更新時候處理,這時候我們需要將 j+1 位置的前綴和由 sum[ j+1 ] 更新為 sum[ j+1] + j*a 。至此,除了查詢 i <= x <= j 範圍的值不正確之外,其余值都能夠保證正確性。
當單點查詢的位置 x 在 i~j 範圍內的時候,我們需要在 x 的位置 單點更新前綴和 sum[ x ] 為 sum[x] +x*a 。現在,我們已經將三個區間都處理結束了,對嗎?但是,這是真的結束嗎? 大家是否還記得單點更新的操作?操作是這樣的:在單點更新的時候,對一個位置 x 的值加 a 時,位置在 x 之前的所有前綴和都是沒有影響的,受影響的是從 x 開始之後的所有位置的前綴和。這也就意味著當單點查詢的位置 x 在 i~j 範圍內的時候,我們對 x 位置的單點更新實際上已經影響到了我們在區間更新時候已經處理好的區間 x > j ,因此我們需要將此處因單點更新加的值 x*a 減去。這也就意味著,我們需要將加數 a 存儲起來,否則,等到單點查詢的時候,加數 a 都已經變成若幹次區間查詢後的加數了。但由於,同一個區間的加數多次疊加後,結果還是正確的。因此,我們可以仿照線段樹時的 lazy_tag 想法存儲每個節點對應的加數(線段樹 lazy_tag相關知識詳見:線段樹第二彈(區間更新))。
對於某一個具體的加數該如何存儲和維護呢?有了前面的分析思路,我們應該可以用兩個樹狀數組分別存儲前綴和與加數,其中一個樹狀數組 lazy_tag來存儲加數,假設某次區間更新的範圍為 i~j ,加數為 a ,我們需要做的操作為:
lazy_tag[i] += a ; //給 x >= i 的所有節點加數均加 a
lazy_tag[j+1] -= a ; // 給 x >j 的所有節點均減 a ,消除 i 位置處理的影響
這樣,就能保證僅對 i ~j 範圍內的加數加了 a ,以此配合區間更新時的操作保證所有前綴和的正確性。此處,對加數的處理和原問題(區間更新)處理思路是一樣的,給某一區間的所有加數均加 a 的問題實質就是區間更新的問題。因此此處,某一位置 x 的加數不是 lazy_tag[x] 的值,而是 lazy_tag 樹狀數組的第 x 位置前綴和。
此時,原數據的前綴和與加數的分布為:
理論知識如上,接下來,我將為大家展示代碼實現部分,希望可以幫助大家理解本篇文章。如下所示:
今天的分享到此結束,大家如果發現文章中寫的不合理的地方,歡迎在下方留言區留言,不勝感激。
沒有關註公眾號的朋友可以長按下圖識別二維碼關註公眾號了解最新文章。
樹狀數組區間更新