树状数组

 

一、引言

  1.什么是树状数组?

    顾名思义,就是用数组来模拟树形结构呗。那么衍生出一个问题,为什么不直接建树?答案是没必要,因为树状数组能处理的问题就没必要建树。和Trie树的构造方式有类似之处。

  2.树状数组可以解决什么问题

    可以解决大部分基于区间上的更新以及求和问题。

  3.树状数组和线段树的区别在哪里

    树状数组可以解决的问题都可以用线段树解决,这两者的区别在哪里呢?树状数组的系数要少很多,就比如字符串模拟大数可以解决大数问题,也可以解决1+1的问题,但没人会在1+1的问题上用大数模拟。

  4.树状数组的优点和缺点

    修改和查询的复杂度都是O(logN),而且相比线段树系数要少很多,比传统数组要快,而且容易写。

    缺点是遇到复杂的区间问题还是不能解决,功能还是有限。

二、基本思想

    算数基本定理:可以将任意正整数关于2的不重复次幂的唯一分解性质,一个数21可以分解为,2^0+2^2+2^4,则一个区间[1,n]可以二进制分解为log(x)个区间

      例如:log(21)=3;

          ①长度为2^4  [1,2^4]

          ②长度为2^2  [2^4+1,2^4+2^2]

          ③[长度为2^0  [2^4+2^2+1,2^4+2^2+2^0]

     树状数组就是依赖算术基本定理的分解思想,把一个区间分解为log(n)个小区间,分而治之

三、基本算法

   若区间结尾为R,则区间长度就等于R的二进制分解下最小的二次幂,这时引入lowbit(n)

   对于序列A,我们建立一个数组c,数组c保存序列A中[x-lowbit[x]+1,x]中所有数的和

   该结构满足一下性质:

    •   每个节点c[x]保存以它为根的子树中所有叶子节点的和
    •   每个内部节点c[x]的子节点个数等于lowbit(x)的大小
    •   除了树根之外,每一个结点c[x]的父亲是c[x+lowbit[x]]
    •   树的深度为log(N)

  1.求lowbit(x)

    原理:将x用二进制表示,将x取反的基础上再加上1,所以原来最后一位到倒着数原来为1的位置(不包含),这些位置原来从0变成1,又因为加上了一,所以就会进位

       原来最后一位到倒着数原来为1的位置(不包含)就会又变成0,直到原来为1的位置与原来是一样的,所以这样就将非负整数x二进制表示下最低位1与后面的0构成的值用lowbit(x)表示

       用因为在补码的表示下~x=-1-n

       所以lowbit(x)=x&(-x);

  2.对某个元素进行加减法操作

    对a[x]对于修改,c[x]以及c[x]的祖先都需要修改,又因为c[x]的祖先为c[x+lowbit(x)],所以可以在log(n)的时间内执行单点增加以及前缀和维护操作

  3.查询前缀和

    对于一个前缀和[1,n],我们把它划分为了log(n)个小区间,想基本思想中举的例子一样,对于区间和,则等于这几个小区间的区间和总值

  4.查询区间和

    利用前缀和求解,sum[i,j]=sum[j]-sum[i-1]

  5.扩展(多维树状数组)

    跟一维的差不多,知识多了几个循环,时间复杂度为(logn)^m,在维度不大的情况下,还是可以接受的

  6.注意事项

    下标不能为0,lowbit(0) ,会陷入死循环

四、典例分析

   模板1(单点修改)

#include <bits/stdc++.h>
using namespace std;

int n,m;
int a[50005],c[50005]; //对应原数组和树状数组

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

void updata(int i,int k){    //在i位置加上k
    while(i <= n){
        c[i] += k;
        i += lowbit(i);
    }
}

int getsum(int i){        //求A[1 - i]的和
    int res = 0;
    while(i > 0){
        res += c[i];
        i -= lowbit(i);
    }
    return res;
}

int main(){
    int t;
    cin>>t;
    for(int tot = 1; tot <= t; tot++){
        cout << "Case " << tot << ":" << endl;
        memset(a, 0, sizeof a);
        memset(c, 0, sizeof c);
        cin>>n;
        for(int i = 1; i <= n; i++){
            cin>>a[i];
            updata(i,a[i]);   //输入初值的时候,也相当于更新了值
        }

        string s;
        int x,y;
        while(cin>>s && s[0] != 'E'){
            cin>>x>>y;
            if(s[0] == 'Q'){    //求和操作
                int sum = getsum(y) - getsum(x-1);    //x-y区间和也就等于1-y区间和减去1-(x-1)区间和
                cout << sum << endl;
            }
            else if(s[0] == 'A'){
                updata(x,y);
            }
            else if(s[0] == 'S'){
                updata(x,-y);    //减去操作,即为加上相反数
            }
        }

    }
    return 0;
}

   模板2(区间修改)

int n,m;
int a[50005] = {0};
int sum1[50005];    //(D[1] + D[2] + ... + D[n])
int sum2[50005];    //(1*D[1] + 2*D[2] + ... + n*D[n])

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

void updata(int i,int k){
    int x = i;    //因为x不变,所以得先保存i值
    while(i <= n){
        sum1[i] += k;
        sum2[i] += k * (x-1);
        i += lowbit(i);
    }
}

int getsum(int i){        //求前缀和
    int res = 0, x = i;
    while(i > 0){
        res += x * sum1[i] - sum2[i];
        i -= lowbit(i);
    }
    return res;
}

int main(){
    cin>>n;
    for(int i = 1; i <= n; i++){
        cin>>a[i];
        updata(i,a[i] - a[i-1]);   //输入初值的时候,也相当于更新了值
    }

    //[x,y]区间内加上k
    updata(x,k);    //A[x] - A[x-1]增加k
    updata(y+1,-k);        //A[y+1] - A[y]减少k

    //求[x,y]区间和
    int sum = getsum(y) - getsum(x-1);

    return 0;
}

五、相关转载与推荐文章(十分感谢这些博主)

    树状数组详解

 

 

 

 

 

l

posted @ 2019-07-31 21:19  SeanOcean  阅读(274)  评论(0编辑  收藏  举报