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

124 lines
4.3 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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