emmmmm, 在我们学习树状数组之前, 我们应该知道lowbit(n)运算, lowbit(n)定义为非负整数n在二进制下“最低位的1及后面所有的0”构成的数值, 例如n = 10的二进制表示为\((1010)_2\),则\(lowbit(n) = 2 = (10)_2\), 显然可知
\[lowbit(n) = n \And (\sim n +1) = n \And (-n)\]
所以我们把一个序列分成\(log(x)\)小区间,
while(x > 0) {
printf("[%d %d]\n", x - (x & -x) + 1, x);
x -= x & -x;
}
树状数组就是基于上述思想的数据结构, 其基本用途是维护序列的前缀和。对于给定的序列a, 我们建立一个数组c, 其中c[x]保存序列a的区间[x - lowbit(x) + 1, x]中所有数的和, 及\(\sum^x_{x - lowbit(x) + 1}a[i]\)
那我们就看树状数组最基本的操作吧
1.单点修改, 区间查询
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某一个数加上x
2.求出某区间每一个数的和
输入格式
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3个整数,表示一个操作,具体如下:
操作1: 格式:1 x k 含义:将第x个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式
输出包含若干行整数,即为所有操作2的结果。
对于查询1~x的前缀和, 我们按照刚才提出的方法, 应该求出x二进制表示中每个等于1的位, 把[1, x]分成\(log(n)\)个小区间, 而每个小区间和都已经保存在数组c中。所以把上边的代码稍加改写即可在\(log(n)\)的时间内查询前缀和:
inline int ask(int x) {
int ans = 0;
for(; x > 0; x -= x & -x) ans += c[x];
return ans;
}
当然, 若要查询区间[l, r]中所有数的和, 只需要计算\(ask(r)-ask(l - 1)\)
对于单点修改, 一个数的改变会影响c[x]即其所有祖先节点保存的"区间和"包括a[x],而任意一个节点的祖先最多有\(log(n)\)个, 我们逐一对它们的c值更新即可。
inline void add(int x, int k) {
for(; x <= n; x += x & -x) c[x] += k;
}
在执行所有操作之前, 我们需要对树状数组进行初始化--针对原始序列a构造一个树状数组
为了简便, 我们一边初始的方法是, 每读入一个a[i], 执行add(i, a[i])的操作, 时间复杂度是\(O(nlogn)\), 通常这种方法已经足够。
还有一种更高效的方法, 用前缀和的方式直接更新c[x], 时间复杂度为\(O(n)\), 不过用这种方法需要多开一个前缀和数组
for(int i = 1; i <= n; ++i) {
read(a[i]);
add(i, a[i]);
}
for(int i = 1; i <= n; ++i) {
read(a[i]);
sum[i] = sum[i - 1] + a[i];
}
for(int i = 1; i <= n; ++i) {
c[i] = sum[i] - sum[i - (i & -i)];
}
2.区间查询, 单点修改
题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数数加上x
2.求出某一个数的值
输入格式
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含2或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x 含义:输出第x个数的值
输出格式
输出包含若干行整数,即为所有操作2的结果。