樹狀數組
爲了表述方便,下面所有的數字,都是二進制形式下的。
拆分成特殊區間------C[i]的定義
樹狀數組通過特定將區間通過一個特殊地規則,將區間拆分成個區間,其中.
而所謂特殊地規則,是這樣的,首先將,首先將用二進制表示,然後將最後一個1變成0即獲得了左端點.
之後把作爲下一個區間的右端點,重複進行,直到整個區間分完了。
舉個例子。假設.劃分的區間如下
l | r |
---|---|
100011001000 | 100011001001 |
100011000000 | 100011001000 |
100010000000 | 100011000000 |
100000000000 | 100010000000 |
000000000000 | 100000000000 |
我們將數二進制表示形式下最後一個1的位權(即那個1及其後面的一連串的0表示的數)定義爲.那麼上面拆分成的特殊區間拆分規則就可以用表示了。
由位運算的知識不難推導出.
可以看到,區間的右端點每次變化的時候,都是去掉一個1.因此,拆分成的子區間數就是i二進制表示下1的個數。假設含有個1,而個1表示的最小數是,即,故,因此.
而樹狀數組的的定義就是區間拆分出來的第一個區間的部分和。
假設所有的C[i]都已知,那麼,對於求前綴和,我們只需要對個進行求和即可。單次前綴和查詢複雜度.
C[i]的求取
現在有個問題,如何求?顯然直接枚舉一個個元素求和是低效的、不明智的。
我們可以充分利用已經前面已經求得的的值。例如表示的區間是.我們將前面兩者相同的前綴10001100簡記爲A.我們可以拆分成:
l | r |
---|---|
A0111 | A1000 |
A0110 | A0111 |
A0100 | A0110 |
A0000 | A0100 |
其中表格第一行所表示的區間只有一個元素,即.剩下的每一行,不難發現表示的區間恰好是.表格的構造也不難,第一行只有一個元素,所以第一行的是.之後每一行的都是上一行的,而這一行的就是.
事實上,也不能說是恰好。因爲這是特意要這樣構造的。這樣拆分,可以充分利用已經求出的並且保證拆分出的不超過的二進制表示下0的個數。表示的區間必然是
的形式。右端點最後有個0,k可以取0.那麼就可以將求變成求和個已經求出來的的和的問題.顯然是.總體C數組的求取是.
爲什麼說是樹狀數組呢?去百度百科裏看一下圖就知道了。就是把表示的區間往求取所要用到各個連一條邊,並且往連一條邊,就變成一棵樹了。最後就變成樹一樣的形式了。而的高度,顯然取決於的二進制表示下的個數(因爲每一層都去掉最後一個1),即至多。因此最高點的高度是。
單點修改a[i]快速更新C數組
到現在爲止,我們通過把區間分解成一個個特殊的區間,並且用一個數組C記錄這一個個小區間。如果僅僅是求前綴和,那麼現在這種做法沒有任何優勢。因爲它多維護了很多不必要的部分區間和。預處理時間複雜度比前綴和差,單次查詢前綴和比前綴和差。
但是,我們的確維護了更多的信息,只是這些多維護的信息暫時還沒有得到充分的利用,所以顯得多餘。
當我們就行元素的單點修改的時候,樹狀數組多維護的部分區間和信息就開始顯示出優勢了。對於樸素的數組前綴和,當我們修改一個元素只是,所有都要更新。而對於樹狀數組而言,由於只是維護了部分區間和,所以修改之時,當且僅當所管轄的區間包含之時才需要更新C[j].
那麼更新時,如何找到需要更新的所有呢?
只需要解決一個問題即可。剛纔是我們求時,是直接找到了直接影響它的所有。即由父親直接找到了所有的兒子及兒子.那麼如何由某一個兒子找到父親呢?
如果是,那麼父親顯然是;如果是c[j],觀察上面的例子,不難發現是父親中的,而通過之前構造出的過程也不難驗證這個猜想正確。
因此,更新的時候,只需要一直往上尋找父親並更新父親的C值,直到發現到頂了。由於每次都是加自己的lowbit往上走,頂再往上走就超過n了。
BTW,其實C[i]的預處理求取也可以直接通過更新操作來求取,a視作全0數組,那麼C數組不論是哪個區間的部分和,和都是0.然後將逐一更新即可。複雜度是一樣的。
小結
樹狀數組C,通過線性的空間複雜度維護一類特殊區間()的部分和信息,以對數的時間複雜度實現了單次自頂向下查詢原數組a前綴和的操作,以對數的時間複雜度實現了單次自底向上的修改a[i]並更新C數組的操作。
Code
樹狀數組的代碼非常簡單,簡直令人髮指。相對於線段樹簡單很多,而且常數比線段樹小不少。當然,線段樹可能多支持一些操作。
inline int lowbit(int x) {return x&(-x);}
inline int lft(int x) {return x&(-x)^x;}
inline int upf(int x) {return x+lowbit(x);}
// 原始數組a下標從1開始
// 樹狀數組 c[i]是初始數組a的部分區間和
// i所掌管的區間是(lft(i),i]
ll a[maxn];
int n;
ll c[maxn];
void add(int i,ll val) {
while (i <= n) c[i] += val, i = upf(i);
}
ll sum(int i) {
ll ans = 0;
while (i) ans += c[i],i = lft(i);
return ans;
}
一些發散的思考
C[i]的更廣泛的定義
前面我們將C[i]定義成了特定區間的區間部分和。但是C[i]不一定要是定義成區間的部分和,只要是滿足結合律的運算的函數應該都可以,例如定義成區間內所有元素的異或值、最值之類的。
但是考慮到單點修改a[i]的時候,我們是把包含的進行更新的。
假設直接指定了修改操作是"+val"(這裏的+不是普通意義的加法,只是代表一種運算)。理論上更新後的的值應該是,而我們修改操作的算法是,即計算的是.理論上的值要和我們實際計算出來的值恆相等就必須使得我們定義的+運算必須滿足交換性。
如果單點修改操作不是直接給出如何修改,而是給出修改後的結果,即直接給出修改後的,則定義的+運算則還需要加上存在逆元(逆操作)這一條。即要變成,如果可交換並且有逆元,那麼就可以計算出目標值。即可以消去原本的影響。例如異或可以,但是求區間最大值卻不可(因爲無逆元)。
當然,如果不要求保持單次修改操作的時間複雜度是,那麼只需要滿足結合性即可。顯然,我們需要修改我們的修改操作的算法。我們依舊是自底向上更新。對於的更新,我們不能利用原本的的信息直接計算,而是用的所有兒子按照其所代表的區間順序(如果不滿足交換性)重新計算一番。另外,對於區間的問題,不能變成與的差的問題,因爲無法消除的影響。於是只能使用逐步分解爲兒子的一個個區間後進行的合併的方法。單點修改複雜度將變成,並且代碼沒那麼優美了.因此,最值問題也可以使用樹狀數組做,只是複雜度多乘以了一個對數。
BTW,事實上,線段樹之所以能處理最值問題就是因爲線段樹採取的是重新用兒子計算。只是,線段樹的一個節點的兒子就只有兩個!
與差分的結合
小結中已經可以看到,樹狀數組支持單點修改,前綴和查詢(之後容易推出區間和)查詢。
那麼區間集體增加一個數,查詢單點值呢?這個引入差分數組即可。區間集體加將等價於差分數組的兩個點的單點修改。查詢單點值將等價於查詢差分數組的前綴和。
如果是區間集體加,查詢區間和呢?依舊引進差分數組b,則前綴和.所以只需要引入數組.如此,區間集體加將是變成c數組的2次單點修改,區間求和將變成c數組的兩次前綴和查詢。再次變成樹狀數組的題目。