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

一、简介

树状数组用于两种操作:

  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;
}
相关推荐
Mephisto.java17 分钟前
【力扣 | SQL题 | 每日四题】力扣2082, 2084, 2072, 2112, 180
sql·算法·leetcode
robin_suli17 分钟前
滑动窗口->dd爱框框
算法
丶Darling.19 分钟前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树
labuladuo52030 分钟前
Codeforces Round 977 (Div. 2) C2 Adjust The Presentation (Hard Version)(思维,set)
数据结构·c++·算法
Indigo_code40 分钟前
【数据结构】【链表代码】合并有序链表
数据结构·windows·链表
jiyisuifeng199140 分钟前
代码随想录训练营第54天|单调栈+双指针
数据结构·算法
我言秋日胜春朝★1 小时前
【C++】红黑树
数据结构
꧁༺❀氯ྀൢ躅ྀൢ❀༻꧂1 小时前
实验4 循环结构
c语言·算法·基础题
新晓·故知1 小时前
<基于递归实现线索二叉树的构造及遍历算法探讨>
数据结构·经验分享·笔记·算法·链表
总裁余(余登武)1 小时前
算法竞赛(Python)-万变中的不变“随机算法”
开发语言·python·算法