[ a ( M ) b ] ( M ) c = a ( M ) [ b ( M ) c ] [a(M)b](M)c=a(M)[b(M)c] [a(M)b](M)c=a(M)[b(M)c]
树状数组是一种数据结构。对于:a 1 , a 2 , a 3 , . . . , a n a_1, a_2, a_3, ... ,a_n a1,a2,a3,...,an
若将其使用树状数组,则会有 ≤ log n \leq \log n ≤logn个摊(分组)。
对摊的定义与解释:
举一个简单的例子:将7写成二进制:7 = ( 111 ) 2 7 = (111)_2 7=(111)2
21 = ( 10101 ) 2 = 1 ∗ 2 4 + 1 ∗ 2 2 + 1 21 = (10101)_2 = 1*2^4 + 1*2^2 + 1 21=(10101)2=1∗24+1∗22+1
7可分为 (前4)+(中2)+(后1)。现在对于 a 1 … a 21 a_1…a_{21} a1…a21来说,可以通过同样的编码方法将其分组:a 1 + . . . a 21 = ( a 1 + . . . + a 16 ) + ( a 17 + . . . + a 20 ) + ( a 21 ) a_1 + ...a_{21} = (a_1 + ...+a_{16}) + (a_{17} + ...+a_{20}) + (a_{21}) a1+...a21=(a1+...+a16)+(a17+...+a20)+(a21)
T 1 : a 1 → a 1 T 2 : a 1 → a 2 T 3 : a 3 → a 3 T 4 : a 1 → a 4 T 5 : a 5 → a 5 T 6 : a 5 → a 6 T 7 : a 7 → a 7 T 8 : a 1 → a 8 T_1: a_1 \rightarrow a_1 \\ T_2: a_1 \rightarrow a_2 \\ T_3: a_3 \rightarrow a_3 \\ T_4: a_1 \rightarrow a_4 \\ T_5: a_5 \rightarrow a_5 \\ T_6: a_5 \rightarrow a_6 \\ T_7: a_7 \rightarrow a_7 \\ T_8: a_1 \rightarrow a_8 T1:a1→a1T2:a1→a2T3:a3→a3T4:a1→a4T5:a5→a5T6:a5→a6T7:a7→a7T8:a1→a8
例如,对于 a 1 , … , a 64 a_1, … , a_{64} a1,…,a64而言,:
长度为64的只有一堆:[1, 64];
长度为32的有2,1堆:[1, 32],[33,64];
长度为16的有4,2堆:[1, 16],[17, 32],[33, 48],[49, 64];
长度为8的有8,4堆:[1, 8],[9, 16],[17, 24],[25, 32],[33, 40],[41, 48],[49, 56],[57, 64];
……等。
对于末尾重复的区间,删去较小的区间,保留较大的区间,以做到节省的目的。在上面的例子当中,可以发现:区间最末尾的值 0 < t i ≤ n 0<t_i\leq n 0<ti≤n,出现且仅出现一次,所以可以发现,总共存在 n n n个区间,所以有 ≤ log n \leq \log n ≤logn个分组
在上面的例子当中,每个摊所管的数量可以得到规律:对于每个 T [ i ] T[i] T[i],其所管的长度为 l b ( i ) lb(i) lb(i)
上面的 l b ( i ) lb(i) lb(i)为“low bit”,为在二进制数当中的最后一个出现的 1 1 1所代表的十进制数,例如:对于12而言, 12 = ( 1100 ) 2 12 = (1100)_2 12=(1100)2,则最后一个 1 1 1为 ( 100 ) 2 = 4 (100)_2 = 4 (100)2=4,则 l b ( 12 ) = 4 lb(12)=4 lb(12)=4。所以 T [ 12 ] T[12] T[12]所管理的区间为 [ 9 , 12 ] [9, 12] [9,12]。
同样地,可以总结出:对于每一个 S [ i ] S[i] S[i],所管的区间为: [ i − l b ( i ) + 1 , i ] [i - lb(i) + 1, i] [i−lb(i)+1,i]。
在树状数组当中,计算的时候,采用上述的摊的计算方式。由此结束了计算的时候的解释。
综合上面的叙述可以不难发现:对于 a 1 , a 2 , a 3 , … , a n a_1, a_2, a_3, … ,a_n a1,a2,a3,…,an而言,一共会定义 n n n个数组, T [ n ] T[n] T[n],但是在运算的时候,会根据实际的需要,选取相关分组的运算,使得时间复杂度为 O ( log n ) O(\log n) O(logn)。
首先,必需要对数组进行初始化(共有 ≤ log n \leq \log n ≤logn个分组):初始化成为目标运算的值,例如加法即为和,求最大值即为各数组的最大值。例如:
for (int i = 1; i <= n; i ++) { cin >> a[i]; add(i, a[i]); }这就是当新的值被添加进来的情况,这等价于将第 i i i号为的值由 0 0 0改为 a i a_i ai,时间复杂度为 log N \log N logN。
对数组进行修改: a d d ( i , Δ ) ⇒ a i → a i + Δ add(i, \Delta) \Rightarrow a_i \rightarrow a_i + \Delta add(i,Δ)⇒ai→ai+Δ:
研究对数组进行改动,必须研究改动所带来的牵连影响,研究对 a i a_i ai数据的改动,将影响那些区间的运算结果。
再举个简单的例子:若对于数组的第九个元素进行改动,则: [ 9 , 9 ] → t [ 9 ] → 9 [ 9 , 10 ] → t [ 10 ] → 9 + l b ( 9 ) = 10 [ 9 , 12 ] → t [ 12 ] → 10 + l b ( 10 ) = 12 [ 1 , 16 ] → t [ 16 ] → 12 + l b ( 12 ) = 16 [9, 9] \rightarrow t[9] \rightarrow 9 \\ [9, 10] \rightarrow t[10] \rightarrow 9 + lb(9) = 10 \\ [9, 12] \rightarrow t[12] \rightarrow 10 + lb(10) = 12 \\ [1, 16] \rightarrow t[16] \rightarrow 12 + lb(12) = 16 [9,9]→t[9]→9[9,10]→t[10]→9+lb(9)=10[9,12]→t[12]→10+lb(10)=12[1,16]→t[16]→12+lb(12)=16
在这个例子当中,会发现上述的区间都会影响,归纳为 t i ’ = t i + l b ( i ) t_i’ = t_i + lb(i) ti’=ti+lb(i)。可以写成以下代码:
void add(int i, int d) { while (i <= n) { T[i] += d; i += lb(i); } }对数组进行查询第 1 − j 1-j 1−j个元素之和: s u m ( j ) ⇒ a 1 + a 2 + … + a j sum(j) \Rightarrow a_1 + a_2 + … + a_j sum(j)⇒a1+a2+…+aj:
再举一个简单的例子:求前23个元素的和: ans + = T [ 23 ] → [ 23 , 23 ] → 23 ans + = T [ 22 ] → [ 21 , 22 ] → 23 − l b ( 23 ) = 22 ans + = T [ 20 ] → [ 16 , 20 ] → 22 − l b ( 22 ) = 20 ans + = T [ 16 ] → [ 1 , 16 ] → 20 − l b ( 20 ) = 16 16 − l b ( 16 ) = 0 \text{ans} += T[23] \rightarrow [23, 23] \rightarrow 23 \\ \text{ans} += T[22] \rightarrow [21, 22] \rightarrow 23 - lb(23) = 22 \\ \text{ans} += T[20] \rightarrow [16, 20] \rightarrow 22 - lb(22) = 20 \\ \text{ans} += T[16] \rightarrow [1, 16] \rightarrow 20 - lb(20) = 16 \hspace{5pt} \\ 16 - lb(16) = 0 ans+=T[23]→[23,23]→23ans+=T[22]→[21,22]→23−lb(23)=22ans+=T[20]→[16,20]→22−lb(22)=20ans+=T[16]→[1,16]→20−lb(20)=1616−lb(16)=0
将上述规律归纳成代码:
int sum(int i) { int ans = 0; while (i > 0) { ans += T[i]; i -= lb(i); } return ans; }对数组进行修改: a d d ( i , Δ ) ⇒ a i → a i + Δ ⇒ T [ m … n ] = max { T [ m … n ] , a [ i ] + Δ } add(i, \Delta) \Rightarrow a_i \rightarrow a_i + \Delta \Rightarrow T[m…n] = \max\{T[m…n] , a[i] + \Delta\} add(i,Δ)⇒ai→ai+Δ⇒T[m…n]=max{T[m…n],a[i]+Δ}
同理:
void add(int i, int d) { while (i <= n) { T[i] = max(T[i], a[i] + d); i += lb(i); } }对数组进行计算取最值: m a x ( j ) ⇒ max { a 1 , a 2 , a 3 , … , a j } max(j) \Rightarrow \max\{a_1, a_2, a_3, …, a_j\} max(j)⇒max{a1,a2,a3,…,aj}
同理:
int max(int i) { int ans = 0; while (i > 0) { ans = max(ans, T[i]); i -= lb(i); } }再论 l b ( i ) lb(i) lb(i):
由于 l b ( i ) lb(i) lb(i)为 i i i转换为二进制后的自后向前数到的第一个 1 1 1和若干个 0 0 0的十进制表示,所以便可以将 l b ( i ) lb(i) lb(i)的计算成 l b ( i ) = i & ( − i ) lb(i) = i\hspace{5pt} \& \hspace{5pt}(-i) lb(i)=i&(−i)。对于 − i -i −i,计算机会对其进行取反加一的操作(负数以其正值得补码形式表达)。例如对于 ( 011011000 ) 2 (011011000)_2 (011011000)2取反: ( 100100111 ) 2 (100100111)_2 (100100111)2,再加一得到 ( 100101000 ) 2 (100101000)_2 (100101000)2。此时的(位运算) i i i与 − i -i −i的与就是 l b ( i ) lb(i) lb(i)的值。例子: ( 011011000 ) 2 & ( 100101000 ) 2 − − − − − − − − − − ( 000001000 ) 2 = 8 \hspace{5pt}\hspace{5pt}(011011000)_2 \\ \&\hspace{5pt}(100101000)_2 \\ ----------\\ \hspace{5pt}\hspace{5pt}\hspace{5pt}\hspace{5pt}\hspace{5pt}\hspace{5pt}(000001000)_2 = 8 (011011000)2&(100101000)2−−−−−−−−−−(000001000)2=8 则 l b ( 216 ) = l b [ ( 011011000 ) 2 ] = 8 lb(216) = lb[(011011000)_2] = 8 lb(216)=lb[(011011000)2]=8 int lb(int i) { return i & (-i); }正因为这个特性:可以将某一个二进制数表达为: … 10 … 0 ( n zeros ) …10…0(n \text{ zeros}) …10…0(n zeros)。
则其的相反数即为: … 01 … 1 ( n ones ) + 1 = … 10 … 0 ( n zeros ) …01…1(n\text{ ones}) + 1 = …10…0(n\text{ zeros}) …01…1(n ones)+1=…10…0(n zeros)。将两数取与,则在第 1 − n 1-n 1−n为皆为 0 & 1 = 0 0 \& 1 = 0 0&1=0,在第 n + 1 n+1 n+1位为 1 & 1 = 1 1\&1 = 1 1&1=1,其后也皆为 0 0 0。(此处的位数顺序为自后向前)
综上所述,可以发现:使用树状数组,无论是添加元素,修改元素,还是查询某位置的结果,其事件复杂度均相同,为 log n \log n logn。