树状数组介绍

【知识点】
树状数组,是一种小巧优雅的数据结构,可在 O(logn) 的时间内计算出数列的前缀和。树状数组,又称二进制索引树。
树状数组的经典实现包含两个数组:一个是存储数列元素的数组 A[],另一个是存储数列前缀和的数组 C[]。而树状数组名称的由来,恰是因为数组 C[] 呈现为树状结构。两个数组之间的关系为:C[i]=A[i-2^k+1]+A[i-2^k+2]+…+A[i],其中的 k 表示 i 的二进制表示末尾有k个连续的 0 。且由 C[] 与 A[] 的关系式易得,每个 C[i] 由数组 A[] 中的 i-(i-2^k+1)+1=2^k 个元素构成。例如,8的二进制表示为1000,其末尾有3个连续的0,则C[8]包含 2^3=8 个 A[] 数组中的元素,即C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8],这也可从树状数组的示意图中明显观察到。


 

在包含9个元素的树状数组中,C[i] 与 A[i] 的对应关系如下:
C[1] = A[1]
C[2] = C[1] + A[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[] 中的元素个数
在编码实践中,每个 C[i] 所包含的数组 A[] 中的元素个数可有下面代码轻松得到。即: 
在定义了lowbit(i)之后,C[i]=A[i-2^k+1]+A[i-2^k+2]+…+A[i],就等价于 A[i−lowbit(i)+1] ~ A[i] 的和。

int lowbit(int i){
    return (-i)&i;    // 返回的值等于上文中的2^k
}

二、直接前驱及直接后继
直接前驱:C[i] 的直接前驱为 C[i-lowbit(i)],即C[i]左侧紧邻的子树的根。
直接后继:C[i] 的直接后继为 C[i+lowbit(i)],即C[i]的父结点。
例如,通过树状数组的示意图,易知C[7]的直接前驱为C[6],C[6]的直接前驱为C[4],C[4]没有直接前驱;
C[5]的直接后继为C[6],C[6]的直接后继为C[8],C[8]没有直接后继。
相应的,C[i]左侧所有子树的根都是C[i]的前驱,C[i]的所有祖先都是C[i]的后继。

三、点更新
若对某个 A[i] 进行修改,如将 A[i] 加上 x,则仅需将 C[i] 及其后继(祖先)都加上 x 便可,而不必对树状数组的所有结点都进行更新。
例如,由于C[5]的后继为C[6]、C[8],所以若将 A[5] 加 2,则仅需将 C[5] 加2、C[6] 加2、C[8] 加2。这通过树状数组的示意图,更容易理解。
树状数组点更新的代码,如下所示:

void update(int i,int val) { //点更新
    while(i<=n) {
        c[i]+=val;
        i+=lowbit(i);  // i的后继(父结点)
    }
}

四、查询前缀和
令 sum(i) 表示 A[] 数组中前 i 元素的前缀和,则 sum(i) 等于 C[i] 加上 C[i] 的前驱。验证如下:
∵ sum(i) = A[1] + A[2] + A[3] + ... + A[i],且有
C[1] = A[1]
C[2] = C[1] + A[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]
∴ sum(1) = A[1] = C[1]     → C[1] 没有前驱
sum(2) = A[1] + A[2] = C[2]     → C[2] 没有前驱
sum(3) = A[1] + A[2] + A[3] = C[3] + C[2]     → C[3] 的前驱是C[2]
sum(4) = A[1] + A[2] + A[3] + A[4] = C[4]     → C[4] 没有前驱
sum(5) = A[1] + A[2] + A[3] + A[4] + A[5] = C[5] + C[4]     → C[5] 的前驱是C[4]
sum(6) = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] = C[6] + C[4]     → C[6] 的前驱是C[4]

sum(7) = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] = C[7] + C[6] + C[4]     → C[7] 的前驱是C[6]、C[4]
sum(8) = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8] = C[8] + C[6] + C[4]     → C[8] 没有前驱
sum(9) = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8] + A[9] = C[9] + C[8]     → C[9] 的前驱是C[8]

…… …… ……

树状数组查询前缀和的代码,如下所示:

int preSum(int i) { //前缀和
    int s=0;
    while(i>0) {  // 树状数组的下标从1开始
        s+=c[i];
        i-=lowbit(i);  // i的前驱
    }
    return s;
}

五、查询区间和
若求区间 [i,j] 的和 A[i] + A[i+1] + … + A[j],利用前缀和的思想可得区间 [i,j] 的和值为 preSum(j)-preSum(i-1)
∵ preSum(j) = A[1] + A[2] + … + A[i-1] + A[i] + … + A[j],
    preSum(i-1) = A[1] + A[2] + … + A[i-1]
∴ preSum(j)-preSum(i-1) = A[i] + A[i+1] + … + A[j],得证。

树状数组查询区间和的代码,如下所示:

int segSum(int i,int j) {
    return preSum(j)-preSum(i-1);
}

六、将 A[x] ~ A[y] 每个元素都加 k
树状数组的经典操作是“单点更新,区间查询”。那么在遇到“洛谷P3368”等“将区间 [x,y] 内每个数加上 k,输出第 x 个数的值”等“区间更新,单点查询”的问题时,怎么办?这就需要利用差分的思想,将“单点更新,区间查询”问题转换为熟悉的“区间更新,单点查询”问题求解。

具体方案为:设原数组为 A[i],定义差分数组 D[i]=A[i]−A[i−1],便可将对数组A[]的“区间更新”操作转化为对差分数组D[]的两次“单点更新”操作。也就是说,此时要将差分数组D[]作为新的原数组构建新的树状数组并实现相关操作。则在新的树状数组中对差分数组D[]的特定“点更新”操作将等效于对原来的原数组A[]所要求进行的“区间更新”操作要注意,树状数组的下标从1开始,则A[0]空置未用,故有 A[0]=0
同时,依据差分数组的定义 D[i]=A[i]−A[i−1] 可知,
D[1]=A[1]−A[0]
D[2]=A[2]−A[1]
D[3]=A[3]−A[2]
......
D[i]=A[i]−A[i-1]
上面各式子相加,可得D[1]+D[2]+D[3]+...+D[i]=A[i]-A[0],又由于A[0]=0,所以可得 A[i]=D[1]+D[2]+D[3]+...+D[i] 
显然,利用上文结论 A[i]=D[1]+D[2]+D[3]+...+D[i] ,可将对数组A[]的“单点查询”操作转化为对差分数组D[]的“区间查询”操作。

下面给出一个具体实例,设数组A[]={1,7,3,6,8,5,9,2,10},依据上文所述具体方案,可得差分数组D[]={1,6,-4,3,2,-3,4,-7,8}。假如对数组A[]的区间[2,6]内的每个元素都加上2,则A[]数组变为A[]={1,9,5,8,10,7,9,2,10},差分数组则变为D[]={1,8,-4,3,2,-3,2,-7,8}。

仔细观察,发现“对数组A[]的区间[2,6]内的每个元素都加上2”这个操作执行后,对应的差分数组D[]中只有D[2]、D[7]的值发生改变。原因是,对数组A[]的区间[x,y]内的每个元素都加上k,将会使 A[x] 与前一个元素 A[x-1] 的差增加 k,A[y+1] 与 A[y] 的差减少 k,且  A[x] ~ A[y] 中其他相邻元素间的差值保持不变。所以,对数组A[]的区间[x,y]内的所有元素进行修改,只用修改D[x]与D[y+1]便可:D[x]=D[x]+k,D[y+1]=D[y+1]-k

显然,依据上述方法,便可将对数组A[]的“区间更新”操作转化为对差分数组D[]的两次“单点更新”操作。此操作需要用到树状数组“点更新”操作 update 的代码(https://blog.csdn.net/hnjzsyjyj/article/details/120559543),相关代码内容如下:

int pre=0;
int val;
for(int i=1; i<=n; i++) { //下标从1开始
    scanf("%d",&val);
    update(i,val-pre); //构造差分数组D[]的树状数组
    pre=val;
}

update(x,k);
update(y+1,-k);



【参考文献】
https://www.cnblogs.com/pigzhouyb/p/10119601.html
https://www.luogu.com.cn/problem/P2073
https://blog.csdn.net/weixin_30245867/article/details/98500495
https://www.cnblogs.com/RabbitHu/p/BIT.html

 

Guess you like

Origin blog.csdn.net/hnjzsyjyj/article/details/120559543