树状数组

定义:

相较于线段树大人,小小的树状数组需要的空间不像线段树那样大♂,一般情况下,仅需 \(O(N)\) 的空间复杂度。

下面是结构示意图:

那个...为什么 \(t[6]\) 包含2个元素,而 \(t[8]\) 包含8个元素呢?这...和见过的树不一样...

就是这样!(激动),树状数组最特别的地方便在这。相较于线段树"层层区间有规律的划分",树状数组你第一眼
还真看不出什么,此时,就要介绍区间规划的核心计算函数————\(lowbit\) 了。

lowbit:\(n.\) 低位(垃圾翻译)

从翻译中可以看的出来,这是跟最低位是有关系的,那么是什么最低?是二进制下最低位的"1".

那么怎么求呢?知道和会用可是两回事。

很简单: lowbit(x) = x & -x .

好简便...为什么?

首先得要知道,这里的-x可不是直接反转,那叫反码,这里的应该是补码

补码在反码的基础上加一,这样使得反码和原码原本可以严丝合缝,1和0互补,最后形成一个全1的数。
但在变成补码后,这么完美的结合被打破了。在反码变成1的位,只能被迫让位反转回去.

那么最后会停在哪呢?会停在第一个0的位:因为它踩到缺口了。

由于且运算同舟共济的特性,使得高位的由于没被再次反转所以都是0,还有低位的零在且运算过后还是0,最后只剩下一个位是1,即所求的lowbit.

那么lowbit应用在哪?重新回到结构图,可以发现,每一位所对应的区间长度,恰好对应其lowbit值
\(lowbit(8) = 8\),因为其二进制为 \(1000_2\) ,\(lowbit(6) = 2\),二进制为 \(110_2\) ,由于只取其最低位,所以结果为2.

这就是如此神奇的一个数据结构...不是吗?

实现:

这里以洛谷的P3374为例,主要有几种操作:

  1. add(x,k) : 在第 \(x\) 位加上 \(k\)

  2. sum(x,y) : 区间 \([x,y]\) 内的元素和。

等等,树状数组不是只能存储一段区间的值吗?

你不会去凑吗?

对于操作二,我们采用前缀和的做法,通过凑来凑出对应完整的前缀区间。

为什么不直接挑对应的区间段呢?

第一,这样的操作导致并不能发挥lowbit的特点,无法通过当前节点的lowbit来判断该取哪一段.

第二,如果你真的要这么做,那么我觉得线段树更适合你.

那么,前缀和怎么求?前面提到,每个位置所记录的区间长度为其lowbit的值,那么减去lowbit,不就是还没被计算到的地方了吗?
所以,这么一直减,减到小于1,就代表已经调完了,不用担心重叠,因为每一个位记录的区间长度为其lowbit值。

板子代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,tree[500005],a[500005],j,x,y,z;
int lowbit(int x){
    return x & -x;
}
void add(int x,int y){ 
    for(int i = x;i <= n;i += lowbit(i)) tree[i] += y;//加的时候确保每一个记录这个位的都要加
    return;
}
int sum(int x){
    int ans = 0;
    for(int i = x;i;i -= lowbit(i)) ans += tree[i]; //i如果为0,直接退出
    return ans;
}
int main(){
    cin >> n >> m;
    for(int i = 1;i <= n;i++) {
        cin >> a[i];
        add(i,a[i]);
    }
    while(m--){
        cin >> x >> y >> z;
        if(x == 1) add(y,z);
        else cout<<sum(z) - sum(y - 1)<<'\n';
    }
}
posted @ 2026-06-14 18:27  Cai_hy  阅读(17)  评论(0)    收藏  举报