樹狀數組從零到一些拓展的學習筆記及模板

樹狀數組

爲了表述方便,下面所有的數字,都是二進制形式下的。

拆分成特殊區間------C[i]的定義

樹狀數組通過特定將區間[1,i]\left[1,i \right]通過一個特殊地規則,將區間拆分成k(klog2i+1)k(k \leq \log_{2}{i+1})個區間(ik1,ik],(ik2,ik1],,(0,i1]]\left(i_{k-1},i_{k} \right], \left(i_{k-2},i_{k-1} \right],\dots,\left(0,i_1\right]],其中ik=ii_k=i.

而所謂特殊地規則,是這樣的,首先將(l,r]\left(l,r\right],首先將rr用二進制表示,然後將最後一個1變成0即獲得了左端點ll.
之後把ll作爲下一個區間的右端點,重複進行,直到整個區間分完了。

舉個例子。假設r=100011001001r=100011001001.劃分的區間如下

l r
100011001000 100011001001
100011000000 100011001000
100010000000 100011000000
100000000000 100010000000
000000000000 100000000000

我們將數ii二進制表示形式下最後一個1的位權(即那個1及其後面的一連串的0表示的數)定義爲lowbit(i)lowbit(i).那麼上面拆分成的特殊區間(l,r]\left(l,r\right]拆分規則就可以用l=rlowbit(r)=rlowbit(r)l=r-lowbit(r)=r^lowbit(r)表示了。

由位運算的知識不難推導出lowbit(x)=x&(x)lowbit(x)=x\&(-x).

可以看到,區間的右端點每次變化的時候,都是去掉一個1.因此,拆分成的子區間數就是i二進制表示下1的個數。假設ii含有kk個1,而kk個1表示的最小數是11111\ldots1,即2k12^k-1,故i2k1i \geq 2^k-1,因此klog2i+1k \leq log_{2}{i+1}.

而樹狀數組的C[i]C[i]的定義就是[1..i][1..i]區間拆分出來的第一個區間的部分和。

假設所有的C[i]都已知,那麼,對於a[1..i]a[1..i]求前綴和,我們只需要對O(log2n)O(log_{2}{n})C[j]C[j]進行求和即可。單次前綴和查詢複雜度O(log2n)O(log_{2}{n}).

C[i]的求取

現在有個問題,如何求C[i]C[i]?顯然直接枚舉一個個元素求和是低效的、不明智的。

我們可以充分利用已經前面已經求得的C[j]C[j]的值。例如C[100011001000]C[100011001000]表示的區間是(100011000000,100011001000]\left( 100011000000, 100011001000\right].我們將前面兩者相同的前綴10001100簡記爲A.我們可以拆分成:

l r
A0111 A1000
A0110 A0111
A0100 A0110
A0000 A0100

其中表格第一行所表示的區間只有一個元素a[A1000]a[A1000],即a[i]a[i].剩下的每一行,不難發現表示的區間恰好是C[A0111],C[A0110],C[A0100]C[A0111],C[A0110],C[A0100].表格的構造也不難,第一行只有一個元素,所以第一行的llr1r-1.之後每一行的rr都是上一行的ll,而這一行的ll就是rlowbit(r)r-lowbit(r).

事實上,也不能說是恰好。因爲這是特意要這樣構造的。這樣拆分,可以充分利用已經求出的C[j]C[j]並且保證拆分出的C[j]C[j]不超過ii的二進制表示下0的個數。C[i]C[i]表示的區間(l,r]\left( l, r\right]必然是
(A00..0,A10..0]\left( A00..0, A10..0\right]的形式。右端點最後有kk個0,k可以取0.那麼就可以將求C[i]C[i]變成求a[i]a[i]kk個已經求出來的C[j]C[j]的和的問題.顯然kkO(log2n)O(log_{2}{n}).總體C數組的求取是O(nlog2n)O(nlog_{2}{n}).

爲什麼說是樹狀數組呢?去百度百科裏看一下圖就知道了。就是把C[i]C[i]表示的區間往求取C[i]C[i]所要用到各個C[j]C[j]連一條邊,並且往a[j]a[j]連一條邊,就變成一棵樹了。最後就變成樹一樣的形式了。而C[i]C[i]的高度,顯然取決於ii的二進制表示下11的個數(因爲每一層都去掉最後一個1),即至多log2i+1log_{2}{i+1}。因此最高點的高度是O(log2n)O(log_{2}{n})

單點修改a[i]快速更新C數組

到現在爲止,我們通過把區間分解成一個個特殊的區間,並且用一個數組C記錄這一個個小區間。如果僅僅是求前綴和,那麼現在這種做法沒有任何優勢。因爲它多維護了很多不必要的部分區間和。預處理時間複雜度O(nlog2(n))O(nlog_2(n))比前綴和O(n)O(n)差,單次查詢前綴和O(log2n)O(log_{2}{n})比前綴和O(1)O(1)差。

但是,我們的確維護了更多的信息,只是這些多維護的信息暫時還沒有得到充分的利用,所以顯得多餘。

當我們就行元素a[i]a[i]的單點修改的時候,樹狀數組多維護的部分區間和信息就開始顯示出優勢了。對於樸素的數組前綴和,當我們修改一個元素a[i]a[i]只是,所有sum[j](ji)sum[j](j \geq i)都要更新。而對於樹狀數組而言,由於只是維護了部分區間和,所以修改a[i]a[i]之時,當且僅當C[j]C[j]所管轄的區間(jlowbit(j),j](j-lowbit(j),j]包含ii之時才需要更新C[j].

那麼更新a[i]a[i]時,如何找到需要更新的所有C[j]C[j]呢?

只需要解決一個問題即可。剛纔是我們求C[i]C[i]時,是直接找到了直接影響它的所有C[j]C[j]。即由父親C[i]C[i]直接找到了所有的兒子C[j]C[j]及兒子a[i]a[i].那麼如何由某一個兒子找到父親呢?

如果是a[i]a[i],那麼父親顯然是c[i]c[i];如果是c[j],觀察上面的例子,不難發現是父親C[i]C[i]中的i=j+lowbit(j)i=j+lowbit(j),而通過之前構造出c[j]c[j]的過程也不難驗證這個猜想正確。

因此,更新a[i]a[i]的時候,只需要一直往上尋找父親並更新父親的C值,直到發現到頂了。由於每次都是加自己的lowbit往上走,頂再往上走就超過n了。

BTW,其實C[i]的預處理求取也可以直接通過更新操作來求取,a視作全0數組,那麼C數組不論是哪個區間的部分和,和都是0.然後將a[1],a[2],a[3],,a[n]a[1],a[2],a[3],\ldots,a[n]逐一更新即可。複雜度是一樣的。

小結

樹狀數組C,通過線性的空間複雜度維護一類特殊區間((ilowbit(i),i]\left( i-lowbit(i), i\right])的部分和信息,以對數的時間複雜度實現了單次自頂向下查詢原數組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]的時候,我們是把包含a[i]a[i]C[j]C[j]進行更新的。

假設直接指定了修改操作是"+val"(這裏的+不是普通意義的加法,只是代表一種運算)。理論上更新後的C[j]C^{'}[j]的值應該是+a[i]+val+\ldots+a[i]+val+\ldots,而我們修改操作的算法是C[i]=C[i]+valC^{'}[i]=C[i]+val,即計算的是+a[i]++val\ldots+a[i]+\ldots+val.理論上的值要和我們實際計算出來的值恆相等就必須使得我們定義的+運算必須滿足交換性。

如果單點修改操作不是直接給出如何修改,而是給出修改後的結果,即直接給出修改後的a[i]a^{'}[i],則定義的+運算則還需要加上存在逆元(逆操作)a-a這一條。即+a[i]+\ldots+a[i]+\ldots要變成+a[i]+\ldots+a^{'}[i]+\ldots,如果可交換並且有逆元,那麼就可以+a[i]+a[i]+a[i]\ldots+a[i]+\ldots-a[i]+a^{'}[i]計算出目標值。即可以消去原本a[i]a[i]的影響。例如異或可以,但是求區間最大值卻不可(因爲無逆元)。

當然,如果不要求保持單次修改操作的時間複雜度是O(log2n)O(log_{2}{n}),那麼只需要滿足結合性即可。顯然,我們需要修改我們的修改操作的算法。我們依舊是自底向上更新。對於C[j]C[j]的更新,我們不能利用原本的C[j]C[j]的信息直接計算,而是用C[j]C[j]的所有兒子按照其所代表的區間順序(如果不滿足交換性)重新計算一番。另外,對於區間a[l,r]a[l,r]的問題,不能變成a[1..r]a[1..r]a[1..l1]a[1..l-1]的差的問題,因爲無法消除a[1..l1]a[1..l-1]的影響。於是只能使用逐步分解爲兒子的一個個區間後進行的合併的方法。單點修改複雜度將變成O(log22n)O({log^2_{2}{n}})並且代碼沒那麼優美了.因此,最值問題也可以使用樹狀數組做,只是複雜度多乘以了一個對數。

BTW,事實上,線段樹之所以能處理最值問題就是因爲線段樹採取的是重新用兒子計算。只是,線段樹的一個節點的兒子就只有兩個!

與差分的結合

小結中已經可以看到,樹狀數組支持單點修改,前綴和查詢(之後容易推出區間和)查詢。

那麼區間集體增加一個數,查詢單點值呢?這個引入差分數組即可。區間集體加將等價於差分數組的兩個點的單點修改。查詢單點值將等價於查詢差分數組的前綴和。

如果是區間集體加,查詢區間和呢?依舊引進差分數組b,則前綴和i=1la[i]=i=1lj=1ib[i]=i=1l[b[i]×(ni+1)]\sum\limits_{i=1}^{l}{a[i]}=\sum\limits_{i=1}^{l}\sum\limits_{j=1}^{i}b[i]=\sum\limits_{i=1}^{l}\left[ b[i] \times (n-i+1) \right].所以只需要引入數組c[i]=b[i]×(ni+1)c[i]=b[i] \times (n-i+1).如此,區間集體加將是變成c數組的2次單點修改,區間求和將變成c數組的兩次前綴和查詢。再次變成樹狀數組的題目。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章