Files
python/TangDou/AcWing/FenwickTree/树状数组.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

4.3 KiB
Raw Blame History

参考视频

### 一、从普通前缀和引入

  • 普通前缀和 采用s_n=a_1+a_2+...+a_n,计算普通前缀和。

  • 动态前缀和 如果在求前缀和的过程中,某个$a_i$修改了,那么原来统计好的$s_i$就不对了,而且可能修改的还不只一个,这样,普通前缀和就不好用了,此时,可以用树状数组来解决。

二、树状数组基础

如上图: $c[1]$管理a[1]; $c[2]$管理c[1],a[2]; $c[3]$管理a[3]; $c[4]$管理c[2],c[3],a[4];

$c[i]$管辖区间范围规定

总结:$i$管辖的区间长度,要看$i$的二进制表示法最后有几个0,比如有$k$个0,它管辖的个数就是$2^k$个。

\large 管辖范围: (a[i]-2^k+1) \sim a[i]

举个栗子:$i=(4)_{10}=(0100)_2$,二进制表示法最后两位是0,所以它的管理范围是2^2=4,即$a[1]\sim a[4]$。

再来一个,i=(8)_{10}=(1000)_2,二进制表示法最后三位是0,所以它的管理范围是$2^3=8$,即$a[1]\sim a[8]$。

$c[i]$管辖区间范围计算办法

如上图,我们想要得$i=20$的最后面的几个0,也就是求最后面的$100$(红色椭圆里面的数字),可以通过二进制运算的$lowbit$运算就可以得到。

总结:$lowbit$函数可以取得原数二进制表示法中最后一个$1$。 $$\large c[i]区间长度计算办法:lowbit(i)=(-i)&i$$

前驱与后继

直接前驱

\large c[i-lowbit(i)]

直接后继

\large c[i+lowbit(i)]

树状数组下标从1开始,不能从0开始。因为数组长度是lowbit(i),如果从$0$开始,lowbit(0)=(-i)\&i=0,那么前驱就是$c[0-0]=c[0]$,就一直不变,死循环。

小结:树状数组下标从$1$开始。

如果我们现在将树状数组记录的是$a[i]$的前缀和数组,那么sum[7]=c[7]+c[6]+c[4] 这是因为$c[7]$管着a[7],$c[6]$管着a[5],a[6],$c[4]$管着$a[1],a[2],a[3],a[4]$。

小结:查询找所有前驱,更新改所有后继。

6.png

7.png

树状数组操作

  • 点更新
  • 查询前缀 两者都是$O(logn)$的时间复杂度。 假设n=1e9,则此时操作次数就是log10^9=30,速度极快。

8.png

三、实现代码

#include <bits/stdc++.h>

using namespace std;
const int N = 1e5 + 10;
int n, a[N];

//下面是用树状数组完成前缀和的保存办法
#define lowbit(x) (x & -x)
int c[N];

//点更新
void add(int i, int z) {
    for (; i <= n; i += lowbit(i)) c[i] += z; //更新所有祖先
}
//前缀和
int sum(int i) {
    int s = 0;
    for (; i; i = i - lowbit(i)) s += c[i];
    return s;
}
//区间和
int sum(int i, int j) {
    return sum(j) - sum(i - 1);
}
/*
测试用例:
5
2 3 1 4 5
*/
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        add(i, a[i]);
    }
    //利用树状数组求前缀和
    printf("%d\n", sum(4));
    //利用树状数组求区间和
    printf("%d\n", sum(5) - sum(3));
    return 0;
}

四、树状数组应用

#### 1、树状数组求区间最大值 https://blog.csdn.net/weixin_46048848/article/details/108529488