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

|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
<font color='blue' size=4><b>树状数组操作</b></font>
|
|
|
* 点更新
|
|
|
* 查询前缀
|
|
|
两者都是$O(logn)$的时间复杂度。
|
|
|
假设$n=1e9$,则此时操作次数就是$log10^9=30$,速度极快。
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
### 三、实现代码
|
|
|
```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
|