【算法】浅谈树状数组
参考资料
概念
从名字就不难看出,树状数组就是一个树形的数组,如下图
其中:
01 02 03 04 为原数组,记为 a 数组
1 2 3 4 为树状数组,记为 t 数组
不难发现,
t[1] = a[1]
t[2] = t[1] + a[2] = a[1] + a[2]
t[3] = a[3]
a[4] = t[2] + t[3] + a[4] = a[1] + a[2] + a[3] + a[4]
但似乎还是找不到规律,我们尝试将它转换成二进制试试
t[1] = a[1]
t[10] = a[1] + a[10]
t[11] = a[11]
t[100] = a[1] + a[10] + a[11] + a[100]
也就是说,从最低位的 1 开始一直加到 1 ,就是 t[i] 的值,即
这时引入 lowbit 的概念,它可以计寻找出最低位的 1
为什么这样就能取到最低位的 1 了呢?
这里先给出一些基本概念,方便不熟悉的读者观看:二进制的原码、反码、补码
我们直接以 7 为例:
7 的二进制为 0111
-7 的二进制为 1001
我们又知道 & 表示:如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
所以 \(7\And(-7)=0001\),就得出了最低位 1
怎么将这个结论具有普遍性呢?这里不妨自己尝试一下
实现
根据上面的分析,我们不难得出查询区间和 (l,r) ,只需利用前缀和即可
int query(int x){
int sum=0;
for(int i=x;i>=1;i-=lowbit(i)) sum+=t[i];
return sum;
}
更改时,由于树状数组存储的是前缀和,所以只需将它上面的所有父节点同时进行更改即可
void update(int x,int k){
for(int i=x;i<=n;i+=lowbit(i)) t[i]+=k;
}
• 单点修改和区间查询
例题:树状数组1
更改时只需:\(update(x,k)\)
查询时只需输出:\(query(r)-query(l-1)\)
将输入的数组看做更改操作即可
• 区间修改与单点查询
例题:树状数组2
这时候我们不能沿用上面那道题的思想了,需要依靠差分的思想来解决
根据差分数组的特性,我们可以得到以下几个规律:
差分数组的前缀和可以得到原来数组的值
将原数组 (l,r) 加上 k 等价于将差分数组第 l 项 +k, 第 r 项 -k
证明这两个规律可以通过举例子的方式
这样我们进行区间修改时只需:\(update(l,k),update(r,-k)\)
查询时只需输出:\(query(x)\)
注意这里都是对差分数组操作
• 区间修改和区间查询
\(\tiny\text{【为什么不试试线段树呢】}\)
例题:线段树1
这时我们发现,如果根据上面的思想,区间修改对差分数组操作,区间查询对原数组操作,肯定是不行的。
我们考虑化简区间查询 (l,r) 的式子:
\(\sum\limits_{i=l}^ra_i = t[l]+t[l]+t[l+1]+……t[l]+t[l+1]+……t[r]\)
\(\qquad \;=n \times t[l] + (n-1) \times t[l+1]+…… 1 \times t[r]\)
\(\qquad \;=\sum\limits_{i=1}^n(n-i+1)\times t[i]\)
\(\qquad \;=n\times \sum\limits_{i=1}^nt[i]-\sum\limits_{i=1}^nt[i]\times(i-1)\)
这样我们可以维护两个数组,一个数组 t[i] 来维护差分,一个数组 pr[i] 维护 \(t[i]\times (i-1)\)
更改时除了之前的基本操作,还要更新 pr[i],一个简单的乘法分配律可得:\(pr[i]+=k\times (x-1);\)
查询时按照我们之前化简出来的式子计算即可