树状数组

需求

  • 随机修改数组中一个数字
  • 求前缀和
  • 以上操作需要频繁操作

预备知识:lowbit

lowbit指数字的二进制最低位1及后续0取出后的数字

1
2
3
int lowbit(int a){
return a & -a;
}
例如:10:1010 & 0110 = 0010,即10的lowbit为2。

x 1 2 3 4 5 6 7 8 9 10
lowbit(x) 1 2 1 4 1 2 1 8 1 2

注意:0不存在lowbit,所以数组的下标需要右偏移一位(使用时偏移或初始化多1位)

树状数组

树状数组
  • tarr从1开始,arr从0开始
  • 树状数组的每一个元素tarr[i]:其值为a[j]+...a[i-1]+a[i-1],其中连加的项总共lowbit(i)
  • 在树状数组中,为什么需要寻找其父节点?有两个用处:1、初始化树状数组。2、修改某个数同时修改其祖上一系列树状数组的值。对于tarr[i]而言,其直接父节点即为tarr[i + lowbit(i)],通过这种方式逐步向上迭代,即可探寻祖上一条链
  • 对于给定数组:arr = [1,3,2,6,4,1],可得其对应的树状数组:tarr = [1, 1+3, 2, 1+3+2+6, 4, 4+1] -> [1, 4, 2, 12, 4, 5]
  • 求和时,例如sum(idx=13)
    • 先得到res += tarr[13],随后idx -= lowbit(idx),此时idx==12
    • 再得到res += tarr[12],随后idx -= lowbit(12),此时idx==8
    • 再得到res += tarr[8],随后idx -= lowbit(8),此时idx==0结束循环
    • 最终,res = tarr[13] + tarr[12] + tarr[8] = (arr[12]) + (arr[8]+...+arr[11]) + (arr[0]+...+arr[7])
  • 总之,对于树状数组tarr
    • idx += lowbit(idx)是为了寻找祖上,修改和初始化用到
    • idx -= lowbit(idx)是为了求和

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class TreeArr {
public:
TreeArr(vector<int> arr) :arr(arr), tarr(arr.size()+1) {
// 初始化树状数组,arr中的每个元素被加到其祖上所有节点中
for (size_t i = 0; i < arr.size(); ++i) {
size_t idx = i + 1; // 偏移1位
while (idx < tarr.size()) {
tarr[idx] += arr[i];
idx += lowbit(idx);
}
}
}

void update(int i, int val) {
int diff = val - arr[i];
size_t idx = i + 1;
// 修改时也要修改其祖上所有节点 + lowbit(idx)
while (idx < tarr.size()) {
tarr[idx] += diff;
idx += lowbit(idx);
}
arr[i] = val; // 勿忘
}

int getSum(int i) {
int idx = i + 1;
int res = 0;
// 求和时则是 - lowbit(idx)
while (idx > 0) {
res += tarr[idx];
idx -= lowbit(idx);
}
return res;
}

int getRange(int l, int r) {
return getSum(r) - getSum(l - 1); //!
}


static int lowbit(int a) {
return a & -a;
}
private:
vector<int> arr;
vector<int> tarr;
};

参考

  1. 什么是树状数组?让这个12岁年轻人为你讲解
  2. 数据结构:树状数组
  3. 307. 区域和检索 - 数组可修改
  4. 剑指 Offer 51. 数组中的逆序对
  5. 315. 计算右侧小于当前元素的个数