如果需要对序列进行以下两种操作:
- 将某一个数加上 x
- 求出某区间每一个数的和
朴素方法肯定不行,使用分块可以降低复杂度,但仍然有 O ( n ) O(\sqrt n) O(n),数据非常大的话就不能过。使用线段树是一种不错的方法,但浪费空间且常数大。
分析发现可以去掉线段树中不需要的结点。具体地说,去掉每一个结点的右儿子,这时线段树就被简化为这个样子:
这种数据结构叫树状数组(BIT),下面讨论如何解决上面的两个问题。
例如:要求[1,5]的和,我们可以求上图中4与5两块的和
同样,要求[1,7]的和,我们可以求上图中7、6、4三块的和
如果将编号转化为二进制,通过观察发现,要求[1,x]的和,只要重复执行下面三个步骤:
1.结果加上BIT[x]
2.取x的最低位1(即执行x&-x),假设得到的数为y
3.令x-=y
直到x=0即可
因此,只要求解两次并将结果相减就可以完成查询。
如果要更新,只要从x开始每次加上y,直到x>n即可。
代码如下:
#include <cstdio>
#include <algorithm>
#define long long long
const int M=(int)5e5;
using namespace std;
long bit[M*2];
int n;
inline int lowbit(int x) {return x&(-x);}
inline void update(int i,long x) {
while(i<=n) {
bit[i]+=x;
i+=lowbit(i);
}
}
inline long query(int i) {
long res=0;
while(i>0) {
res+=bit[i];
i-=lowbit(i);
}
return res;
}
int main() {
int m;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) {
long x;
scanf("%lld",&x);
update(i,x);
}
//1为更新,2为查询
while(m--) {
short op;
int l;
long r;
scanf("%hd%d%lld",&op,&l,&r);
if(op==1) update(l,r);
else printf("%lld\n",query(r)-query(l-1));
}
return 0;
}
下面讨论如何用树状数组高效完成下面的操作:
- 将某区间每一个数数加上 x;
- 求出某一个数的值。
直接用树状数组无法完成。但发现树状数组有明显的类似前缀和的特性。因此将数组转化为差分(此处指前缀和逆运算)数组,修改[l,r]内的值等价于修改差分数组l,r+1两点的值。由于此处是加法,因此差分数组中的第l项+1,第r+1项-1。
代码如下:
#include <cstdio>
#include <algorithm>
#define lowbit(x) (x)&(-(x))
#define long long long
const int M=(int)5e5;
using namespace std;
long bit[M*2];
int n;
inline void update(int i,long val) {
while(i<=n) {
bit[i]+=val;
i+=lowbit(i);
}
}
inline long query(int i) {
long res=0;
while(i>0) {
res+=bit[i];
i-=lowbit(i);
}
return res;
}
int main() {
int m;
scanf("%d%d",&n,&m);
long x,last=0;
for(int i=1; i<=n; i++) {
scanf("%lld",&x);
update(i,x-last); //记住树状数组里存放的不是实际值
last=x;
}
while(m--) {
char op;
scanf("%hhd",&op);
if(op==1) {
int x,y,k;
scanf("%d%d%d",&x,&y,&k);
update(x,k);
update(y+1,-k);
}
else {
int x;
scanf("%d",&x);
printf("%lld\n",query(x));
}
}
return 0;
}
附: 下面给出树状数组求逆序对的方法:
1.对给定数据a[]进行离散化。注意为了处理重复数据,必须用第一种方法离散化,且应进行稳定排序。
2.对每一个a[i],先执行将树状数组第a[i]个元素+1(update(a[i],1)),再将结果加上a[i]减去树状数组前a[i]项的和(query(a[i]))。原理:求逆序对,可以看成对于每一个位置上的数,求在它前面且比它大的数总共有多少个。
将树状数组中的数看成每个数出现的次数,这样,对于任意一个数a[i],树状数组中前a[i]项的和就是之前已经更新过的(即位置在a[i]前面)且不比a[i]大的数字的个数,用a[i]在原数组中升序排列的序数减去它,即得到以逆序对个数。为了减少空间浪费,故进行离散化。