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

一、简介

树状数组用于两种操作:

  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 tx tx表示区间 x − l o b i t ( x ) + 1 , x x-lobit(x)+1, x x−lobit(x)+1,x的和 ,则 t 0 t0 t0~ t 8 t8 t8如下所示
可以看到形成一个类似树的结构,例如t8的孩子就有 t 4 , t 6 , t 7 t4, t6, t7 t4,t6,t7


这个树有如下性质:

性质2:

通过父节点 t x tx tx找子节点:令 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 t8 t8,得到 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 t111_2,t110_2,t100_2 t1112,t1102,t1002也就是 t 7 , t 6 , t 4 t7,t6,t4 t7,t6,t4

通过性质2,可以由孩子求出父亲 ,例如 t 8 = a 8 + t 4 + t 6 + t 7 t8=a8+t4+t6+t7 t8=a8+t4+t6+t7

查询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;
}
相关推荐
复杂网络4 小时前
多个 Claude Code 与多个 Codex 协同工作:设计与实现方案
算法
HjhIron19 小时前
面试常客:字符串算法从入门到进阶
算法·面试
吴佳浩21 小时前
DeepSeek DSpark:Confidence-Scheduled Speculative Decoding 技术解析
人工智能·算法·deepseek
触底反弹1 天前
🧠 搞懂 Token,才算真正入门大模型——从分词原理到 Embedding 语义实战
javascript·人工智能·算法
vivo互联网技术1 天前
ICLR 2026 | 基于后验采样的图像恢复方法LearnIR:人脸去阴影、去雾
人工智能·算法·aigc
浮生望1 天前
JS字符串与回文算法:从包装类到双指针的面试进阶之路
javascript·算法
黄敬峰1 天前
面试必刷:从JS底层包装类到双指针,彻底搞懂字符串与回文算法
算法
地平线开发者2 天前
J6B vio scenario sample
算法
BothSavage2 天前
Trae远程开发中DeepSeek自定义模型4054错误的排查与修复
算法