树状数组
树状数组,正如起名是一个逻辑上为树结构的数组,其结构如下图所示

可见其二进制位从低位到高位,0的个数决定该结点在第几层,例如结点1(00001),从低位到高位有0个0,所以在第0层,结点4(00100),从低位到高位有2个0,所以在第2层
通过观察,还能看到,一个结点的父结点是该结点加上lowbit(x)
树状数组t[i] 存储的是(x - lowbit(x), x]的值(左开右闭)
其中lowbit(x) 表示的是能够返回一个二进制序列从最后一位1的结果
如果x为10100,取反后结果是01011,再加一则变为了01100,再与10100做与运算后变为00100

了解了树状数组的结构,那么树状数组有什么应用
- 单点修改
- 区间查询
这两个是树状数组最基本的应用
单点修改的操作是在单点修改的同时,迭代更新父节点
java
/**
* 在x个数加上k
*/
int add(int x,int k)
{
for(int i=x;i<=n;i+=lowbit(i))
t[i]+=k;
}
区间查询就是迭代加上t[i],每次i -= lowbit(i)
java
/**
* 查询从第1-x个数的和
*/
int query(int x) {
int res = 0;
for(int i = x; i > 0; i -= lowbit(i)) {
res += tree[i];
}
return res;
}
【例题】
给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b] 的连续和。
输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。
第二行包含 n 个整数,表示完整数列。
接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。
数列从 1 开始计数。
输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。
数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8
输出样例:
11
30
35
java
import java.io.*;
import java.util.*;
public class Main {
static final int N = 100010;
//原数组
static int[] a = new int[N];
//树状数组
static int[] tree = new int[N];
//实际树状数组长度
static int n;
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] row1 = br.readLine().split(" ");
n = Integer.parseInt(row1[0]);
int m = Integer.parseInt(row1[1]);
//读取原数组,从下标1开始
String data[] = br.readLine().split(" ");
for(int i = 1; i <= n; i++) {
a[i] = Integer.parseInt(data[i - 1]);
}
for(int i = 1; i <= n; i++) {
add(i, a[i]);
}
while(m-- > 0) {
String[] op = br.readLine().split(" ");
int o = Integer.parseInt(op[0]);
int x = Integer.parseInt(op[1]);
int y = Integer.parseInt(op[2]);
if(o == 1) {
add(x, y);
} else if(o == 0) {
int res = query(y) - query(x - 1);
System.out.println(res);
}
}
br.close();
}
static int lowbit(int x) {
return x & (~x + 1);
}
static void add(int x, int v) {
for(int i = x; i <= n; i += lowbit(i)) {
tree[i] += v;
}
}
static int query(int a) {
int res = 0;
for(int i = a; i > 0; i -= lowbit(i)) {
res += tree[i];
}
return res;
}
}
操作 | 时间复杂度 |
---|---|
add | O(logn) |
query | O(logn) |
与前缀和算法的比较
前缀和算法无法实现动态求前缀和,如果只需要求一次前缀和之后不再修改,前缀和算法自然比树状数组要好
因为前缀和算法时间复杂度如下
操作 | 时间复杂度 |
---|---|
init | O(n) |
query | O(1) |