zoukankan      html  css  js  c++  java
  • 树状数组初探

    【引言】假如我们有一个长度为N的数组,我们需要频繁地进行如下两种操作:

    (1)修改下标为 i 的某个元素的值;

    (2)查询下标 L 到 R的区间的元素和。

    如果我们采用朴素的算法,很显然,操作1的时间复杂度是O(1),频繁修改一点问题没有,但是操作2的时间复杂度为O(R - L),试想一下,如果频繁地查询效率是极低的。比如说N = 108 查询次数M = 106,这样总的时间复杂度达到O(NM)1014,是根本不能忍受的。

    【问题分析】首先我们分析一下采用朴素的算法它究竟慢在哪里。比如说我之前修改了a[7] 现在我要查询a[8] ~ a[10],我每次查询区间和都要重新算一遍。那么我们可不可以把区间和事先存起来,存在另外一个数组 C[]中,这样我们在查询区间和的时候就直接O(1)地调用就好了。

    存储任意L - R的区间和是不现实的,这个C数组要开成二维,空间占用1016

    【问题解决】有人提出来可以构造一个a数组的前缀和数组,这样查询L-R区间的和就相当于求C[R] - C[L-1] !真是个好主意。

    我们来考察这个想法合不合适,查询复杂度我们降到了O(1),那么修改的复杂度呢?

    我们光修改原数组肯定是不行的,如果要维持查询区间和为O(1)的复杂度,那么每次对原数组单点修改的时候必须对C数组进行相应地修改。我们可以思考一下,如果我们修改了a[7],那么我们要修改C数组的那些项,很显然,C[1] ~ C[6]是不用改的,因为他们不包含a[7]。但是对于C[7] C[8] C[9].......我们都要进行修改,改多少呢,增加一个差量,比如说我把a[7]从9改成5,那么C[7] C[8] C[9]......都要增加一个-4.

    我们可以看出,尽管求区间和的复杂度降为O(1)但修改的复杂度提高至O(N)。这是不行的。

    【进一步解决】

    我们引入一种很优雅的数据结构:树状数组。

    我们先不给出树状数组的构建规则,先看一下前面的特殊情况推理出一般性的规律。

    观察C数组和A数组的关系,我们可以发现:

    C[1] = A[1] 

    C[2] = A[1] + A[2] 

    C[3] = A[3]

    C[4] = C[2] + C[3] + A[4] = A[1] + A[2] + A[3] + A[4] ;

    C[5] = A[5]

    C[6] =C[5] + A[6] = A[5] + A[6] 

    C[7] = A[7] 

    C[8] = C[4] + C[6] + C[7] +A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] +A[8]

    C[9] = A[9]

    【构建规则】我们从上述特例中找找C[i]和A数组有什么关系。我们把 i 写成二进制的形式。

    比如C[4] , C[100] = A[100] + A[11] + A [10] + A[1] 

    C[5],  C[101] = A[101]

    C[6] , C[110] = A[110] + A[101]

    C[7] , C[111] = A[111]

    不难发现,求C[i] 时候, 如果把 i 的最低位的1所代表的的权值求出,记为K,那么C[i] 就是 A[i] + A[i - 1] + ......+ A[i - K + 1]。也就是从A[i]开始往左数K个元素直到A[i - K + 1],把这些值加起来。 

    比如 C[6], C[110] ,最低位1权值为K = 2,6 - 2 + 1 = 5,所以最多加到A[5]  ,所以C[6] = A[6] + A[5]

    这就是树状数组C[i] 相对于原数组A[i] 的构建规则。求C[i] 时候, 如果把 i 的最低位的1所代表的的权值求出,记为K,那么C[i] 就是 A[i] + A[i - 1] + ......+ A[i - K + 1]

    【查询与修改的方法】

    一、查询区间和方法

    设SUM(x)是区间1到X的元素和。

    假如我们要求SUM(7)怎么求?是不是就是C[4] + C[6] + C[7]?

    这是怎么实现的呢?我们将7写成2进制 ,SUM(111) = C[111] + C[110] + C[100]

    再举一个例子, 求SUM(6)  SUM(110) = C[110] + C[100]

     SUM(9),  SUM(1001) = C[1001] + C[1000]

    我们可以发现,设SUM(X) = C[P1] + C[P2] + ......+ C[Pk]   =   

    那么Pi是Pi-1的值减去Pi-1最低位那一位的1所代表的的权值,直到P= 0为止。仔细验算一下是不是?

     所以在求SUM(X) 时, SUM(X) =  (其中Pi是不断变化的,Pi是上一次求出的Pi 减去其最低位1代表的权值)

    二、单点修改方法

    我们知道在修改原数组某一个位置的值时,树状数组必须要进行相应的变化。比如说把A[i]的值增加 X, 那么树状数组哪些元素需要增加呢?

    那就要看哪些C[j] 控制了A[i],根据上面所总结出的树状数组的构建规则,我们可以知道:C[i]要增加X, C[i + K]需要增加 X(K是i最低位的1所代表的权值), 然后i 更新为 i+K, K的值也根据i更新

    C[i+K]需要增加X,继续更新i = i + K,更新K......一直迭代下去直到i = N (肯定不能超出数组长度啊)

    【子问题提出】:不论在构建C数组还是在查询区间和的过程中,都需要计算这样 一个东西:求数X的最低位的1所代表的的权值。

    那么如何求一个二进制数最后一个1所代表的的权值呢?

    原理:给定一个正数X, 先求出-X(正数补码还是本身,负数补码就是把负数原码第一个1和最后一个1之间的所有位全部取反,其余位不变。)

    比如6,6的相反数是-6 ,-6原码是10110,它在计算机中是以补码形式表示的,补码是11001 + 1 = 11010  其实也就是原码的第一个1和最后一个1之间的位全部取反然后其他位不变。

    再进行X & (-X)   (结合补码特点,这就是为什么可以取出最低位的1的原因)

    也就是 (00110) & (11010) = 10 = 2,这就把最低位的1所代表的的权值计算出来了。

    【代码实现】

    原数组a[i]  树状数组 c[i] 下标从1开始

    (1)取X最低位1所代表的权值的方法  lowbit(int x)

    int lowbit(int x){
        return x & (-x);
    }

     (2)单点修改方法

     

    void add(int x , ll val){
        while(x <= n){
            c[x] += val;
            x += lowbit(x);
        }
    }

    (3)区间查询 1~X

    int query(int x){
        int  ans = 0;
        while(x > 0){
            ans += c[x];
            x -= lowbit(x);
        }
        return ans;
    }

    我们求SUM(L,R)只需要query(R) - query(L - 1)即可

    【完整代码】

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int maxn = 1e6+10; 
    int  a[maxn];
    int n,m;
    int  c[maxn];
    
    void init(){
        memset(a , 0 , sizeof a);
        memset(c , 0 , sizeof c);
    }
    int lowbit(int x){
        return x & (-x);
    }
    
    int  query(int x){
        int  ans = 0;
        while(x > 0){
            ans += c[x];
            x -= lowbit(x);
        }
        return ans;
    }
    
    void add(int x , int  val){
        while(x <= n){
            c[x] += val;
            x += lowbit(x);
        }
    }
    
    int main(){
        while(cin>>n>>m){
            init();
            for(int i=1; i<=n; i++){
                cin>>a[i];
                add(i , a[i]);
            }
            int op,L,R,pos,X;
            for(int i=1; i<=m; i++){
                cin>>op;
                //如果是查询操作 
                if(op == 1){
                    cin>>L>>R; 
                    cout<<query(R) - query(L-1)<<endl;
                }
                //如果是修改操作 
                else{
                    cin>>pos>>X;
                    add(pos , X - a[pos] );
                    a[pos] = X;
                }
            }
            
        }
        return 0;
    }
  • 相关阅读:
    从零开始写STL—哈希表
    从零开始写STL-string类型
    从零开始写STL—模板元编程之any
    从零开始写STL—模板元编程之tuple
    c++ 实现 key-value缓存数据结构
    从零开始写STL
    从零开始写STL—functional
    从零开始写STL—set/map
    从零开始写STL-二叉搜索树
    洛谷 P4016 负载平衡问题
  • 原文地址:https://www.cnblogs.com/czsharecode/p/9624418.html
Copyright © 2011-2022 走看看