1.什么是树状数组
树状数组(Binary Indexed Tree,BIT)是能够完成下述操作的数据结构。
给一个初始值全为0的数列a1,a2,···,an
- 给定i,计算a1+a2+···+ai
- 给定i和x,执行ai+=x
2.BIT的结构
如图,其中A为普通数组,C为树状数组(C在物理空间上和A一样都是连续存储的)。
从图中可以得出:
C1 = A1
C2 = C1 + A2 = A1 + A2
C3 = A3
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
C5 = A5
C6 = C5 + A6 = A5 + A6
C7 = A7
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
再看一下线段树
发现什么规律没,我们可以发现把线段树上每个节点的右儿子都去掉后,再把剩下的节点对应到数组中得到的数据结构就是BIT了。比起线段树,BIT实现起来更方便,速度也更快。
再回到树状数组,让我们对比每个节点对应的区间的长度和节点编号的二进制表示。以1结尾的1,3,5,7的长度是1,最后有1个0的2,6的长度是2,最后有2个0的4的长度是4······这样,编号的二进制表示就能够和区间非常容易地对应起来。利用这个性质,BIT可以通过非常简单的位运算实现:
-
BIT的求和
计算前i项的和需要从i开始,不断把当前位置i的值加到结果中,并从i中减去i的二进制最低非0位对应的幂(lowbit(i)),直到i变成0为止。i的二进制的最后一个1可以通过i&-i得到。 -
BIT的值的更新
使第i项的值增加x需要从i开始,不断把当前位置i的值增加x,并把i的二进制最低非0位对应的幂加到i上。
3.BIT的复杂度
总共需要对O(log n)个值进行操作,所以复杂度是O(log n)。
4. BIT的实现
老规矩,上模板题 原题传送门
c++实现代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5e5+7;
int n,m,a[maxn]; //树状数组
int sum(int i){ //求前i项的和
int s=0;
while(i){
s+=a[i];
i-=i&-i;
}
return s;
}
void add(int i,int x){ //执行ai+=x
while(i<=n){
a[i]+=x;
i+=i&-i;
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int t;
cin>>t;
add(i,t);
}
while(m--){
int p,x,y;
cin>>p>>x>>y;
if(p==1) add(x,y); //单点更新
else cout<<sum(y)-sum(x-1)<<endl; //计算从s到t的和等于计算(从1到t的和)-(从1到s-1的和)
}
return 0;
}
5.BIT的应用
有前人总结得非常好了,这里转载一下
原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/78922383
1、PUIQ模型
【例题1】一个长度为n(n <= 500000)的元素序列,一开始都为0,现给出三种操作:
1. add x v : 给第x个元素的值加上v; ( a[x] += v )
2. sub x v : 给第x个元素的值减去v; ( a[x] -= v )
3. sum x y: 询问第x到第y个元素的和; ( print sum{ a[i] | x <= i <= y } )
这是树状数组最基础的模型,1和2的操作就是对应的单点更新,3的操作就对应了成端求和。
具体得,1和2只要分别调用add(x, v)和add(x, -v), 而3则是输出sum(y) - sum(x-1)的值。
我把这类问题叫做PUIQ模型(Point Update Interval Query 点更新,段求和)。
2、IUPQ模型
【例题2】一个长度为n(n <= 500000)的元素序列,一开始都为0,现给出两种操作:
1. add x y v : 给第x个元素到第y个元素的值都加上v; ( a[i] += v, 其中 x <= i <= y )
2. get x: 询问第x个元素的值; ( print a[x] )
这类问题对树状数组稍微进行了一个转化,但是还是可以用add和sum这两个函数来解决,对于操作1我们只需要执行两个操作,即add(x, v)和add(y+1, -v);而操作2则是输出sum(x)的值。
这样就把区间更新转化成了单点更新,单点求值转化成了区间求和。
我把这类问题叫做IUPQ模型(Interval Update Point Query 段更新,点求值)。
原题点这里
参考代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5e5+7;
int n,m,a[maxn],b[maxn];
int sum(int i){
int s=0;
while(i){
s+=a[i];
i-=i&-i;
}
return s;
}
void add(int i,int x){
while(i<=n){
a[i]+=x;
i+=i&-i;
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>b[i];
while(m--){
int p,x,y,k;
cin>>p;
if(p==1){
cin>>x>>y>>k;
add(x,k);
add(y+1,-k);
}
else{
cin>>x;
cout<<b[x]+sum(x)<<endl;
}
}
return 0;
}
3、逆序模型
【例题3】给定一个长度为n(n <= 500000)的排列a[i],求它的逆序对对数。1 5 2 4 3 的逆序对为(5,2)(5,3)(5,4)(4,3),所以答案为4。
朴素算法,枚举任意两个数,判断他们的大小关系进行统计,时间复杂度O(n2)。不推荐!
来看一个给定n个元素的排列 X0 X1 X2 … Xn-2 Xn-1,对于某个 Xi 元素,如果想知道以它为"首"的逆序对的对数( 形如(XiXj) 的逆序对),就是需要知道 Xi+1 … Xn-2 Xn-1 这个子序列中小于 Xi 的元素的个数。
那么我们只需要对这个排列从后往前枚举,每次枚举到 Xi 元素时,执行cnt += sum(Xi-1),然后再执行add(Xi, 1),n个元素枚举完毕,得到的cnt值就是我们要求的逆序数了。总的时间复杂度O(nlogn)。
这个模型和之前的区别在于它不是将原数组的下标作为树状数组的下标,而是将元素本身作为树状数组的下标。逆序模型作为树状数组的一个经典思想有着非常广泛的应用。
原题点这里
参考思路: 逆序模型是根据元素本身作为树状数组下标来建的,此题ai<109,显然空间不够,所以需要对数据离散化,先将数据排序,再用 1~ n 分别对应 n个数表示它们的相对大小,对新的序列建树状数组空间就够了(n≤5×105)。另外排序的时候要注意相等的元素,不处理的话会出错的,问题的关键在于是否有与 ai相等的元素在 ai前被加入且其相对大小标记更大。出现这种情况就会误将两个相等的数判为逆序对。怎么解决呢,只要所有与 ai相等的元素中,先出现的标记也更小就好了(我们只统计相对更大的)。具体只需要在排序时将 ai作为第一关键字,下标(第几个出现)作为第二关键字从小到大排序即可。
具体c++实现代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e5+7;
ll n,ans=0,a[maxn],b[maxn];
struct node{
int val,num;
}c[maxn];
bool cmp(node a,node b){
if(a.val==b.val) return a.num<b.num;
return a.val<b.val;
}
ll sum(int i){
ll s=0;
while(i){
s+=a[i];
i-=i&-i;
}
return s;
}
void add(int i,int x){
while(i<=n){
a[i]+=x;
i+=i&-i;
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>c[i].val;
c[i].num=i;
}
sort(c+1,c+n+1,cmp);
for(int i=1;i<=n;i++)
b[c[i].num]=i;
for(int i=n;i;i--){
ans+=sum(b[i]);
add(b[i],1);
}
cout<<ans;
return 0;
}
4、多维树状数组模型
【例题4】给定一个N*N(N <= 1000)的矩形区域,执行两种操作:
1. add x y v 在(x, y)加上一个值v;
2. sum x1 y1 x2 y2 统计矩形(x1, y1) - (x2, y2)中的值的和;
PUIQ模型的二维版本。我们设计两种基本操作:
1. add(x, y, v) 在(x, y)这个格子加上一个值v;
2. sum(x, y) 求矩形区域(1, 1) - (x, y)内的值的和,那么(x1,y1)-(x2,y2)区域内的和可以通过四个求和操作获得,即 sum(x2, y2) - sum(x2, y1 - 1) - sum(x1 - 1, y2) + sum(x1 - 1, y1 - 1)。 (利用容斥原理的基本思想)
add(x, y, v)和sum(x, y)可以利用二维树状数组实现,二维树状数组可以理解成每个C结点上又是一棵树状数组(可以从二维数组的概念去理解,即数组的每个元素都是一个数组),具体代码如下:
void add(int x,int y,int v){
for(int i = x; i <= n; i += lowbit(i)){
for(int j = y; j <= n; j += lowbit(j)){
c[i][j]+= v;
}
}
}
int sum(int x,int y){
int s =0;
for(int i = x; i ; i -= lowbit(i)){
for(int j = y; j ; j -= lowbit(j)){
s += c[i][j];
}
}
return s;
}
仔细观察即可发现,二维树状数组的实现和一维的实现极其相似,二维仅仅比一维多了一个循环,并且数据用二维数组实现。那么同样地,对于三维的情况,也只是在数组的维度上再增加一维,更新和求和时都各加一个循环而已。