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

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

[参考视频](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