什么是树状数组

        首先我们搞明白树状数组是用来干嘛的,现在有一个这样的问题:有一个数组a,下标从0n-1,现在给你w次修改,q次查询,修改的话是修改数组中某一个元素的值;查询的话是查询数组中任意一个区间 [left,right] 的和。

        这个问题很常见

  • 首先分析下朴素做法的时间复杂度,修改是O (1) 的时间复杂度,而查询的话是O(n)的复杂度,总体时间复杂度为 O(qn);
  • 可能你会想到前缀和来优化这个查询,我们也来分析下,查询的话是O(1)的复杂度,而修改的时候修改一个点,那么在之后的所有前缀和都要更新,所以修改的时间复杂度是O(n),总体时间复杂度还是O(qn)。

接下来我们来看一下树状数组的做法。

        这里我们先不管树状数组这种数据结构到底是什么,先来了解下lowbit(x)这个函数,我们也先不要问这个函数到底在树状数组中有什么用;

顾名思义,lowbit这个函数的功能就是求某一个数的二进制表示中最低的一位1,举个例子,x = 6,它的二进制为110,那么lowbit(x)就返回2,因为最后一位1表示2

在这里,我们提供两种lowbit的实现方法

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

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

树状数组的思想

        在树状数组的问题模型中已经有所提及了,就是那两种不同做法的一个综合;

        先定义一些东西:arr是原数组,c是新的一个数组,这个数组代表后缀和;

        二进制的视角:一个数n,假设n = 6,它的二进制为110,那么我们要求前6项的和是不是可以这样求:

\sum_{i=1}^{6}=(arr_{1}+arr_{2}+arr_{3}+arr_{4})+(arr_{5}+arr_{6})

        注意括号中的元素个数,是不是4(100)个加2(10)个,和110 = 100 + 10是不是很像,不知你们发现了吗,10就是lowbit(110)的结果,100lowbit(100)的结果。求和的时候我们总是把​\sum_{i=1}^{n}拆分成这样的几段区间和来计算,而如何去确定这些区间的起点和长度呢?就是根据n的二进制来的(不懂的可以再看下上面举的例子),二进制怎么拆的,你就怎么拆分,而拆分二进制就要用到上面说的lowbit函数了。这里也可以顺理成章得给出c数组的表示了。 

        这里也可以顺理成章得给出c数组的表示了,c[i]表示从第i个元素向前数lowbit(i)个元素,这一段的和,这就是上面说的区间和,只不过这个区间是靠右端点的;你可能又会想,不是说区间是靠右端点的吗,是后缀和啊,那中间的这些区间怎么定义?其实递归定义就好了,比如说

\sum_{i=1}^{6}=(arr_{1}+arr_{2}+arr_{3}+arr_{4})+(arr_{5}+arr_{6})=\sum_{i=1}^{6}=(arr_{1}+arr_{2}+arr_{3}+arr_{4})+c[6]

因此我们可以得出\sum_{i=1}^{4}=c[4]=c[6-lowbit(6)]

lowbit(1) = 0001 = 1 即 c[1]代表前一个元素的和
lowbit(2) = 0010 = 2 即 c[2]代表前两个元素的和
lowbit(3) = 0011 = 1 即 c[3]代表前一个元素的和
lowbit(4) = 0100 = 4 即 c[4]代表前四个元素的和
lowbit(5) = 0101 = 1 即 c[5]代表前一个元素的和
lowbit(6) = 0110 = 2 即 c[6]代表前两个元素的和
lowbit(7) = 0111 = 1 即 c[7]代表前一个元素的和
lowbit(8) = 1000 = 8 即 c[8]代表前八个元素的和

 

树状数组的实现

        设计一种数据结构,需要的操作无非就是”增删改查“,这里只讨论查询和修改操作具体是怎么实现的;

查询

这里说的查询是查询任一区间的和,由于区间和具有可加减性,故转化为求前缀和;

        查询前缀和刚刚在树状数组的思想中已经说过了,就是把大区间分成几段长度不等的小区间,然后求和。区间的个数为O(logn),所以查询的时间复杂度为O(logn)。

修改

        修改某一位置上的元素的时间复杂度为O(1),但是要更新c数组,不然查询的时间复杂度就会变高。

        更新的时候只要更新修改这个点会影响到的那些后缀和(c数组),假设现在修改6(110)这个点,依据树状数组的性质,它影响的直系父层就是c[6(110) + lowbit(6(110))] = c[8(1000)],但是它肯定不是只影响直系父层,上面所有包含这一层和的层都要更新,但是我们把这个更新传递给直系父层c[8],8这个点的直系父层是c[16],依次类推地更新就行了。

接着我们来看树状数组的实现

 给你一个数组 nums ,请你完成两类查询。

  1. 其中一类查询要求 更新 数组 nums 下标对应的值
  2. 另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的  ,其中 left <= right
class NumArray {
private:
    vector<int> tree;
    vector<int>& nums;
    int lowBit(int x)
    {
        return x&-x;
    }

    void add(int index,int val)//对应修改操作
    {
        while(index < tree.size())
        {
            tree[index] += val;
            index += lowBit(index);
        }
    }

    int prefixSum(int index)//对应查询操作
    {
        int sum = 0;
        while(index > 0)
        {
            sum+=tree[index];
            index -= lowBit(index);
        }
        return sum;
    }
public:
    NumArray(vector<int>& nums):tree(nums.size()+1),nums(nums)
    {
        for(int i=0;i<nums.size();++i)
        {
            add(i+1,nums[i]);
        }
    }
    
    void update(int index, int val) {
        add(index+1,val-nums[index]);
        /*
            这里的 val - nums[index]是什么意思呢?
            假设nums[index]原来的值是x,现在我们要将他修改为y
            那么我们就可以让x加上(y-x)让它变为y
            即 x+(y-x) = y
        */  
        nums[index] = val;
    }
    
    int sumRange(int left, int right) {
        return prefixSum(right+1)-prefixSum(left);
    }
};

猜你喜欢

转载自blog.csdn.net/ThinPikachu/article/details/123954343
今日推荐