【刷题】数据结构——树状数组

一、简介

树状数组用于两种操作:

  1. 快速求前缀和 O ( l o g n ) O(logn) O(logn)
  2. 修改某一个数 O ( l o g n ) O(logn) O(logn)

这两个操作也可以用其他方法结构完成:

用一个数组存每个数:操作1. O ( n ) O(n) O(n),遍历前n个数求和;操作2. O ( 1 ) O(1) O(1),直接修改即可

维护前缀和一个数组:操作1. O ( 1 ) O(1) O(1);操作2. O ( n ) O(n) O(n)

可见其他结构最坏情况都是 O ( n ) O(n) O(n)

二、算法

已知一个数可以拆成若干个2的幂次之和
x = 2 i 1 + 2 i 2 + 2 i 3 + . . . + 2 i k x=2^{i_1}+2^{i_2}+2^{i_3}+...+2^{i_k} x=2i1+2i2+2i3+...+2ik

其中 i 1 ≤ i 2 ≤ . . . ≤ i k i_1 \leq i_2 \leq ... \leq i_k i1≤i2≤...≤ik

所以区间 ( 0 , x ] (0, x] (0,x]可以拆成

( x − 2 i 1 , x ] (x-2^{i_1},x] (x−2i1,x]
( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] (x-2^{i_1}-2^{i_2},x-2^{i_1}] (x−2i1−2i2,x−2i1]

.

.

.
( x − 2 i 1 − 2 i 2 − . . . − 2 i k , x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 ] (x-2^{i_1}-2^{i_2}-...-2^{i_k},x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}] (x−2i1−2i2−...−2ik,x−2i1−2i2−...−2ik−1]

也就是
( 0 , x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 ] (0,x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}] (0,x−2i1−2i2−...−2ik−1]

再来看这些区间包含多少个数
( x − 2 i 1 , x ] (x-2^{i_1},x] (x−2i1,x]包含 2 i 1 2^{i_1} 2i1个数,其中 i 1 i_1 i1是 x x x二进制表示的最后一位1的位置
( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] (x-2^{i_1}-2^{i_2},x-2^{i_1}] (x−2i1−2i2,x−2i1]包含 2 i 2 2^{i_2} 2i2个数,其中 i 2 i_2 i2是 x − 2 i 1 x-2^{i_1} x−2i1二进制表示的最后一位1的位置
( 0 , x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 ] (0,x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}] (0,x−2i1−2i2−...−2ik−1]包含 2 i k 2^{i_k} 2ik个数,其中 i k i_k ik是 x − 2 i 1 − 2 i 2 − . . . − 2 i k − 1 x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}} x−2i1−2i2−...−2ik−1二进制表示的最后一位1的位置。

性质1:对于区间 ( L , R ] (L,R] (L,R],区间包含的数的个数是R的二进制表示的最后一位1的位置对应次幂。

这个最后一位1的位置对应次幂 可以用lowbit(x) = x & (-x)计算求得,因为计算机里的负数就是对原数的二进制取反+1

例如: 6 = 11 0 2 6=110_2 6=1102, − 6 = 01 0 2 -6=010_2 −6=0102,lowbit(6) = 110 & 010 = 10,即6最后一位1对应次幂是 1 0 2 = 2 1 10_2=2^1 102=21

再来看如何拆分区间: x = 6 = 2 2 + 2 1 x=6=2^2+2^1 x=6=22+21,二进制是 11 0 2 110_2 1102,因此 i 1 = 1 i_1=1 i1=1, i 2 = 2 i_2=2 i2=2

所以区间 ( 0 , 6 ] (0, 6] (0,6]可以拆成:
( 6 − 2 1 , 6 ] = ( 4 , 6 ] (6-2^1,6]=(4,6] (6−21,6]=(4,6],其中 i 1 = 1 i_1=1 i1=1是 6 6 6二进制表示 110 110 110最后一位1的位置
( 6 − 2 1 − 2 2 , x − 2 2 ] = ( 0 , 4 ] (6-2^1-2^2,x-2^2]=(0,4] (6−21−22,x−22]=(0,4],其中 i 2 = 2 i_2=2 i2=2是 4 4 4二进制表示 100 100 100最后一位1的位置

有了性质1,可以得到 L = R − l o b i t ( R ) L=R-lobit(R) L=R−lobit(R),故区间为 ( R − l o b i t ( R ) , R ] (R-lobit(R),R] (R−lobit(R),R]


用数组 t [ x ] t[x] t[x]表示区间 [ x − l o b i t ( x ) + 1 , x ] [x-lobit(x)+1, x] [x−lobit(x)+1,x]的和 ,则 t [ 0 ] t[0] t[0]~ t [ 8 ] t[8] t[8]如下所示
可以看到形成一个类似树的结构,例如t[8]的孩子就有 t [ 4 ] , t [ 6 ] , t [ 7 ] t[4], t[6], t[7] t[4],t[6],t[7]。


这个树有如下性质:

性质2:

通过父节点 t [ x ] t[x] t[x]找子节点:令 x ′ = x − 1 x'=x-1 x′=x−1,之后不断去掉 x ′ x' x′的最后一位1即可。

即对于 x = . . . 10...00 0 2 x=...10...000_2 x=...10...0002,得到 x ′ = x − 1 = . . . 01...11 1 2 x'=x-1=...01...111_2 x′=x−1=...01...1112,所以有孩子 . . . 01...11 1 2 ...01...111_2 ...01...1112、 . . . 01...11 0 2 ...01...110_2 ...01...1102、 . . . 01...10 0 2 ...01...100_2 ...01...1002、 . . . 01...00 0 2 ...01...000_2 ...01...0002、...、 . . . 00...00 0 2 ...00...000_2 ...00...0002

例如 t [ 8 ] t[8] t[8],得到 x ′ = 100 0 2 − 1 2 = 11 1 2 x'=1000_2-1_2=111_2 x′=10002−12=1112,则孩子有 t [ 11 1 2 ] , t [ 11 0 2 ] , t [ 10 0 2 ] t[111_2],t[110_2],t[100_2] t[1112],t[1102],t[1002]也就是 t [ 7 ] , t [ 6 ] , t [ 4 ] t[7],t[6],t[4] t[7],t[6],t[4]

通过性质2,可以由孩子求出父亲 ,例如 t [ 8 ] = a [ 8 ] + t [ 4 ] + t [ 6 ] + t [ 7 ] t[8]=a[8]+t[4]+t[6]+t[7] t[8]=a[8]+t[4]+t[6]+t[7]

查询a[1] + ... + a[x] -> for(int i = x; i > 0; i -= lobit(i)) sum += t[i]

性质3:

通过子节点找父节点:将父节点找子节点过程逆过来即可

对于孩子 x = . . . 01...10 0 2 x=...01...100_2 x=...01...1002,父亲一定是 . . . 10...00 0 2 ...10...000_2 ...10...0002,可以看到孩子只有唯一一个父亲

所以只需 . . . 01...10 0 2 + . . . 00...10 0 2 ...01...100_2+...00...100_2 ...01...1002+...00...1002即可得到父亲 . . . 10...000 ...10...000 ...10...000
也就是x + lobit(x)

通过性质3,可以找到更新原数组时所需要更新的对应的树状数组

原数组修改a[x] += c -> 树状数组修改for(int i = x; i <= n; i += lobit(i)) t[i] += c

注意:树状数组维护的位置下表只可以从1开始,如果为0则的lowbit也是0,会死循


三、例题

树状数组模板

注意

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 500005;
int n, m;
int t[N], a[N];


int lowbit(int x) {
    return x & (-x);
}

int sum(int x) {
    int s = 0;
    for (int i = x; i > 0; i -= lowbit(i))   s += t[i];
    return s;
}

void add(int x, int c) {
    for (int i = x; i <= n; i += lowbit(i)) {
        t[i] += c;
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) {
        scanf("%d", &a[i]);
        add(i, a[i]);
    }
    int op, x, y;
    for (int i = 1; i <= m; i ++ ) {
        scanf("%d%d%d", &op, &x, &y);
        if (op == 1) {
            add(x, y);
        }
        else {
            printf("%d\n", sum(y) - sum(x - 1));
        }
    }
	return 0;
}
相关推荐
CoovallyAIHub7 小时前
中科大DSAI Lab团队多篇论文入选ICCV 2025,推动三维视觉与泛化感知技术突破
深度学习·算法·计算机视觉
NAGNIP8 小时前
Serverless 架构下的大模型框架落地实践
算法·架构
moonlifesudo8 小时前
半开区间和开区间的两个二分模版
算法
moonlifesudo8 小时前
300:最长递增子序列
算法
CoovallyAIHub13 小时前
港大&字节重磅发布DanceGRPO:突破视觉生成RLHF瓶颈,多项任务性能提升超180%!
深度学习·算法·计算机视觉
CoovallyAIHub13 小时前
英伟达ViPE重磅发布!解决3D感知难题,SLAM+深度学习完美融合(附带数据集下载地址)
深度学习·算法·计算机视觉
聚客AI1 天前
🙋‍♀️Transformer训练与推理全流程:从输入处理到输出生成
人工智能·算法·llm
大怪v1 天前
前端:人工智能?我也会啊!来个花活,😎😎😎“自动驾驶”整起!
前端·javascript·算法
惯导马工1 天前
【论文导读】ORB-SLAM3:An Accurate Open-Source Library for Visual, Visual-Inertial and
深度学习·算法
骑自行车的码农2 天前
【React用到的一些算法】游标和栈
算法·react.js