有这样一个问题:给定一个n个元素的数组{A},你的任务是设计一个数据结构,支持一下操作:
ADD(x,d):让Ax增加d
Query(L,R)求A[L]+A[L+1]+...+A[R]的和
很显然这可以用线段树求解,但这里说的不是这种数据结构,我们要讨论的是树状数组,一个利用位运算来进行操作的数据结构,相对于上述问题,树状数组速度略微会快一点,而且写起来也简单一些,当然线段树的用处更多。
首先需要知道这样一个结论:x&(-x)表示的是x二进制表示最右边的1所对应的权值,如6&(-6)=2,这是因为计算机中数字使用补码表示,6取负后各位取反,然后末尾加1,那么原先末尾连续的0变成1后加1又变成0,直至末尾第一个1的位置,其余位置相与为0,。我们记lowbit[i]=(i&(-i));
树状数组的示意图如下
从图上易知如果某一节点编号为i,那么其父亲节点编号为i+lowbit[i],我们建立一个辅助数组C[i],其中C[i]=A[i-lowbit[i]+1]+...+A[i],即C[i]表示{A}的某一段和是哪一段呢?即是图上C[i]统治的左边部分,如C[4]为A[1]+A[2]+A[3]+A[4],C[3]为A[3],那么有了C[i],如何求某一段和呢?很简单,我们用S[i]表示前缀和,即是S[i]=A[1]+...+A[i],则sum(L,R)=S[R]-S[L-1],
求S[i]即可。而S[i]可以这样来求:从C[i]开始往左上走,如求S[7],顺序为C[7],C[6],C[4],加上某一个数时,同样如A[3]+d,那么顺序更新C[i]数组,往右上走C[3],C[4],C[8],看出来了吧,更新其实就是按照i+lowbit[i]和i-lowbit[i]的标号来更新的,那么代码也就呼之欲出了:
C++代码:
int sum(int x){
int ret=0;
while(x>0){
ret+=C[x];x-=lowbit(x);
}
return ret;
}
void add(int x,int d){
while(x<=n){
C[x]+=d;x+=lowbit(x);
}
}
问题举例:
有n个乒乓球爱好者,他们的技术被量化为数字,他们排成顺序的一列,如果要进行比赛,必须有三个人,切这三个人的顺序和序列中的顺序要一致,即不能调换相对顺序,中间的人的值必须在左右两人之间,求这样的比赛个数。
很容易想到的是对于每一个人,枚举在他左边有多少个小于他的值,右边有多少个小于他的值,那么即可求出这个人当裁判的比赛场数,每一个人都这样处理即可求出最后解。关键是如何求左边或者右边小于他的个数。我们这样来考虑,对于某一个人,记他的技术值为x,如果在他左边有小于他的技术值得人,那么值必然在1~x中,这很容易想到一个数组标记它,于是利用一个数组x[j],x[j]记为从扫描开始到现在是否存在ai=j的标记,那么小于第i个人的技术值得人的个数就为x[1]+x[2]+...+x[a[i]-1],记前缀和,这就可以利用树状数组来处理,而右边一样重新扫描一遍即可得出右边小于该人技术值的个数,答案也就呼之欲出了。
RMQ问题(范围最小问题)
给出一个n个元素的数组A1,A2,...,An,求min{A[L],A[L+1],...,A[R]}
怎么办呢,我们可以先设d(i,j)表示i开头的长度为2^j的一段元素中的最小值,那么地推关系式有d(i,j)=min(d(i,j-1),d(i+2^(j-1),j-1))
那么我们可以这样初始化d(i,j)数组
C++代码:
void RMQ_init(const vector<int>& A){
int n=A.size();
for(int i=0;i<n;i++)d[i][0]=A[i];
for(int j=1;(1<<j)<=n;j++){
for(int i=0;i+(1<<j)-1<n;i++)
d[i][j]=min(d[i][j-1],d[i+(1<<(j-1))][j-1]);
}
}
查询可以这样查询:
int RMQ(int L
int RMQ(int L,int R){
int k=0;
while((1<<(k+1))<<(R-L+1))k++;//找到最大的k使得2^k<=(R-L+1)
return min(d[L][k],d[R-(1<<k)+1][k]);
}
问题举例:
给出一个非降序的序列a1,a2,...,an,询问(i,j)这一段中出现次数最多的值所出现的次数。
应该注意到的是这是一个非降序的序列,那么我们就可以将这个序列写成(a[k1],k1),(a[k2],k2)...的形式,其中ki表示其出现的次数,那么序列就被分为几个段,对于询问(i,j)我们需要知道这样几个信息来解题,位置i所在的段num[i],位置i所在段的右端点right[i],位置j所在的段num[j],位置j所在段的左端点left[j],那么答案就很容易求出了,即为段num[i]+1~num[j]-1的每个段中的最大值,right[i]-i+1,j-left[j]+1三者中的较大值,而第一个值得求解即是我们上述讨论的RMQ问题。