20190509:学校-3-树状数组

树状数组


  • 树状数组的作用:计算一些数据的从1开始计算到 i i i位置的和、从1位置到 i i i位置的最大值,等。这些运算都必须满足有聚合运算的性质,(满足结合律)。例如,对于求最大值,定义 a ( M ) b a (M) b a(M)b为求 a a a b b b的最大值,与加法的结合律相同,由结合律存在下式:

[ 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=124+122+1

  • 7可分为 (前4)+(中2)+(后1)。
  • 现在对于 a 1 … a 21 a_1…a_{21} a1a21来说,可以通过同样的编码方法将其分组:

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:a1a1T2:a1a2T3:a3a3T4:a1a4T5:a5a5T6:a5a6T7:a7a7T8:a1a8

  • 例如,对于 a 1 , … , a 64 a_1, … , a_{64} a1,,a64而言,:

    • 长度为64的只有一堆:[1, 64];

    • 长度为32的有21堆:[1, 32]​,[33,64]​

    • 长度为16的有42堆:[1, 16]​,[17, 32],​[33, 48]​,[49, 64]

    • 长度为8的有84堆:[1, 8],[9, 16],[17, 24],[25, 32],[33, 40],[41, 48],[49, 56],[57, 64]

      ……等。

  • 对于末尾重复的区间,删去较小的区间,保留较大的区间,以做到节省的目的。在上面的例子当中,可以发现:区间最末尾的值 0 &lt; t i ≤ n 0&lt;t_i\leq n 0<tin,出现且仅出现一次,所以可以发现,总共存在 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] [ilb(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


  1. 对数组进行求和的操作
  • 对数组进行修改: a d d ( i , Δ ) ⇒ a i → a i + Δ add(i, \Delta) \Rightarrow a_i \rightarrow a_i + \Delta add(i,Δ)aiai+Δ

    • 研究对数组进行改动,必须研究改动所带来的牵连影响,研究对 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 1j个元素之和: 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]23lb(23)=22ans+=T[20][16,20]22lb(22)=20ans+=T[16][1,16]20lb(20)=1616lb(16)=0

    • 将上述规律归纳成代码:

    int sum(int i) {
        int ans = 0;
        while (i > 0) {
            ans += T[i];
            i -= lb(i);
        }
        return ans;
    }
    

  1. 对数组进行最大值的操作:
  • 对数组进行修改: 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,Δ)aiai+ΔT[mn]=max{T[mn],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 &amp; ( − i ) lb(i) = i\hspace{5pt} \&amp; \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 &amp; ( 100101000 ) 2 − − − − − − − − − − ( 000001000 ) 2 = 8 \hspace{5pt}\hspace{5pt}(011011000)_2 \\ \&amp;\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}) 100(n zeros)

    则其的相反数即为: … 01 … 1 ( n  ones ) + 1 = … 10 … 0 ( n  zeros ) …01…1(n\text{ ones}) + 1 = …10…0(n\text{ zeros}) 011(n ones)+1=100(n zeros)。将两数取与,则在第 1 − n 1-n 1n为皆为 0 &amp; 1 = 0 0 \&amp; 1 = 0 0&1=0,在第 n + 1 n+1 n+1位为 1 &amp; 1 = 1 1\&amp;1 = 1 1&1=1,其后也皆为 0 0 0。(此处的位数顺序为自后向前)

  • 综上所述,可以发现:使用树状数组,无论是添加元素,修改元素,还是查询某位置的结果,其事件复杂度均相同,为 log ⁡ n \log n logn

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值