You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

124 lines
4.3 KiB

2 years ago
[参考视频](https://www.bilibili.com/video/BV12a411i7rP)
### 一、从普通前缀和引入
* **普通前缀和**
采用$s_n=a_1+a_2+...+a_n$,计算普通前缀和。
* **动态前缀和**
如果在求前缀和的过程中,某个$a_i$修改了,那么原来统计好的$s_i$就不对了,而且可能修改的还不只一个,这样,普通前缀和就不好用了,此时,可以用树状数组来解决。
### 二、树状数组基础
<center><img src='https://cdn.acwing.com/media/article/image/2022/07/09/64630_0c10d5b6ff-1.png'></center>
如上图:
$c[1]$管理$a[1]$;
$c[2]$管理$c[1],a[2]$;
$c[3]$管理$a[3]$;
$c[4]$管理$c[2],c[3],a[4]$;
<font color='blue' size=4><b>$c[i]$管辖区间范围规定</b></font>
<center><img src='https://cdn.acwing.com/media/article/image/2022/07/09/64630_940bb2eeff-2.png'></center>
<font color='red' size=4><b>
总结:$i$管辖的区间长度,要看$i$的二进制表示法最后有几个$0$,比如有$k$个$0$,它管辖的个数就是$2^k$个。</b></font>
<font color='blue' size=4><b>
$$\large 管辖范围: (a[i]-2^k+1) \sim a[i]$$
</b></font>
**举个栗子**:$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]$。
<font color='blue' size=4><b>$c[i]$管辖区间范围计算办法</b></font>
<center><img src='https://cdn.acwing.com/media/article/image/2022/07/09/64630_3ab5075bff-3.png'></center>
如上图,我们想要得$i=20$的最后面的几个$0$,也就是求最后面的$100$(红色椭圆里面的数字),可以通过二进制运算的$lowbit$运算就可以得到。
<font color='red' size=4><b>总结:$lowbit$函数可以取得原数二进制表示法中最后一个$1$。</b></font>
<font color='blue' size=4><b>
$$\large c[i]区间长度计算办法lowbit(i)=(-i)\&i$$</b></font>
<font color='blue' size=4><b>前驱与后继</b></font>
<center><img src='https://cdn.acwing.com/media/article/image/2022/07/09/64630_9df270b7ff-5.png'></center>
<font color='black' size=4><b>直接前驱</b></font>
$$\large c[i-lowbit(i)]$$
<font color='black' size=4><b> 直接后继</b></font>
$$\large c[i+lowbit(i)]$$
树状数组下标从1开始不能从0开始。因为数组长度是$lowbit(i)$,如果从$0$开始,$lowbit(0)=(-i)\&i=0$,那么前驱就是$c[0-0]=c[0]$,就一直不变,死循环。
<font color='red' size=4><b>小结:树状数组下标从$1$开始。</b></font>
如果我们现在将树状数组记录的是$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]$。
<font color='red' size=4><b>小结:查询找所有前驱,更新改所有后继。</b></font>
![6.png](https://cdn.acwing.com/media/article/image/2022/07/09/64630_8c38e0caff-6.png)
![7.png](https://cdn.acwing.com/media/article/image/2022/07/09/64630_8e7398ccff-7.png)
<font color='blue' size=4><b>树状数组操作</b></font>
* 点更新
* 查询前缀
两者都是$O(logn)$的时间复杂度。
假设$n=1e9$,则此时操作次数就是$log10^9=30$,速度极快。
![8.png](https://cdn.acwing.com/media/article/image/2022/07/09/64630_e38dd464ff-8.png)
### 三、实现代码
```c++
#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