树状数组的简单理解及其用处

一,前言:

什么是树状数组?顾名思义,就是用数组来模拟树形结构。那么衍生出一个问题,为什么不直接建树?因为没必要,树状数组能处理的问题就没必要建树。

假设你要对一个数组进行查询和修改,那么树状数组能为你提供O(logN)的修改和查询的时间复杂度,这里的查询是指查询任一区间的和(也就包含了单点查询),修改是指单点修改。

而普通数组操作时,查询区间和的时间复杂度是O(n),修改的时间复杂度是O(1);

当需要进行m次查询和n次修改时,树状数组的优势就显现出来了。

树状数组的优势:

1,单点更新、区间查询——时间复杂度分别为O(logN),O(logN)

2,区间更新、单点查询——时间复杂度分别为O(logN),O(logN)

3,区间更新,区间查询——时间复杂度分别为O(logN),O(logN)

4,单点更新,单点查询——用普通数组即可

二,树状数组是什么?

这是二叉树:

变形一下:

取每列的最高结点为树状数组的结点。 A是原数组,C是树状数组,那么:

假设C[i]最左端的子孙为C[j],那么C[i] = sum(A[j],A[i]),其中,sum(A[j],A[i])表示数组A在区间[j,i]上的累加。

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

为了简单理解,有必要明确一个事实,那就是每一个C[i]和它的区间[j,i]之间存在函数对应关系。

为了说明这个函数关系,先引入一个东西:

假设lowbit(i) = k,k代表i对应的二进制数最低位1的数位。比如,12对应的二进制数就是1100,那么它的最低位1的数位是从右往左第3位,所以lowbit(12)=2^(3-1)=4;同理,lowbit(8)=8,lowbit(6)=2.

那么C[i]对应的区间[j,i]的长度=lowbit(i);

也就是说,j = i - lowbit(i) + 1。

怎么求lowbit(i)呢?

主要有两种办法:

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

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

主要说第二种办法:

利用的负数的存储特性,负数是以补码存储的,对于整数运算 x&(-x)有
当x为0时,即 0 & 0,结果为0;
当x为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。结果为1。
当x为偶数时,假设为1100,那么取反加一就等于0100。
取反时,最低位1右边的所有0全部变成1,最低位1变成0,加上1之后,就会导致最低位(原来的最低位1,下面的最低位均指这个意思)右边的1全部变成0,而最低位又变回了1.这个变化不会波及最低位左边的数,所以最低位左边的数仍是取反状态,所以与运算的结果就是最低位1的值

这个lowbit函数的时间复杂度是O(1)。

树状数组的更新和查询操作:

更新操作:当更新原数组的一个元素时,通常会波及所有包含这个元素的树状数组元素。

会波及哪些元素呢?如果要更新A[i],那么就会波及C[i]和C[i+lowbit(i)]。再将i+lowbit(i)作为i一直迭代下去就行了。

那么为什么是C[i+lowbit(i)]呢?可以用对称的思想来理解:

比如C[6] 和C[i+lowbit(i)],也就是C[6]和C[8]:

 可以发现,将C[i+lowbit(i)]下移一定距离后,以C[i+lowbit(i)]为根结点和以C[i]为根结点的二叉树,它们的子结点数量是一致的(包含的树状数组结点的数量也是一致的)

所以,对于任意的C[j]来说,它包含lowbit(j)个A数组的元素,而它右边过去lowbit(j)的位置,就是更新时会波及的结点。

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

查询操作:

我们所说的查询,是指查询从A[1]到A[i]的前缀和,而不是某个中间区间的和,数组A中间区间的和可以通过对两个前缀和作差得到。

我们可以知道,C[i]仅仅包含一部分A数组元素的和,不一定是前缀和。C[i]只包含lowbit(i)个元素的和,所以我们用i-lowbit(i)就能跳出C[i]包含元素的边界。递归地进行上述过程即可覆盖前缀和。

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

二,单点更新、区间查询

已经在第一部分里面谈过了,就不再展开讲了

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;
}

三,区间更新,单点查询

这里就要用到差分数组了。简单说一下差分数组:

假设A[n]是一个含有n个元素的数组,那么定义:D[0]=A[0],D[i] = A[i]-A[i-1],i>0时。那么D[n]就是数组A对应的差分数组。例如:

  • A[] = 1 2 3 5 6 9
  • D[] = 1 1 1 2 1 3

当A数组的区间[i,j]里面所有元素同时加上一个数时,我们只需要改变D[i]和D[j+1]的值就能表示这个操作。

我们把[2,5]区间内值加上2,则变成了

  • A[] = 1 4 5 7 8 9
  • D[] = 1 3 1 2 1 1

相应地,我们用A数组对应的差分数组D来建立树状数组,那么就可以实现区间更新,单点查询。

此时区间更新就变成了两点更新,运用树状数组的updata(int i,int k)就能将时间复杂度变为O(logN).

此时的单点查询就是差分数组前缀和,可以用树状数组求前缀和的方式得到,时间复杂度为O(logN)。

四,区间更新,区间查询

此时可以这样想:假设原数组为A[n],那么A数组在区间[0,j]上面的和就等于A[0]+A[1]+...A[j]。其中的A[k]=D[0]+D[1]+...D[k].

展开依次求和,可得A[0]+A[1]+...A[j] = (j+1)*D[0] + j*D[1] + ...D[j]

=(j+1)*A[j] - D[1] - 2*D[2] - 3*D[3] .....-j*D[j]

所以我们可以对通项为i*D[i]和通项为D[i]的数组分别建立树状数组。那么,我们可以通过D[i]数组对应的树状数组在O(logN)的时间内算出第一项,后面的项可以用另一个树状数组在O(logN)的时间内算出,查询出A数组在区间[0,j]上面的和仅仅需要O(logN)的时间。

同时,对数组A的区间进行更新时,只需要更改D数组的两个元素,这就会导致两个树状数组只需要更新两个元素,时间复杂度依然为O(logN)

如有错误,敬请指正,礼貌交流,感激不尽

参考资料:https://www.cnblogs.com/xenny/p/9739600.html

猜你喜欢

转载自blog.csdn.net/fly_view/article/details/129816579
今日推荐