引入
对于一个数列S=a1+a2+...+ak+...+an,我们有以下操作
1.区间求和:如要算[3,n-1]区间的和,则可以用前缀和Sn-1 - S3
2.对于ak,我们要加上d,则可以直接对ak进行操作
对于单步操作,如区间求和,它的时间复杂度为O(n),更新某个值为O(1)。但这仅仅是对于单步操作而言,若允许两个操作都进行,则时间复杂度就不止于此。于是,我们引进了树状数组,它的时间复杂度只有O(log2n)。
具体操作
首先,我们定义数组sum为前缀和,再定义数组tree(其作用后面会讲),ai为具体的值。首先看一张图:
图中,我们可以清晰地知道,tree[1]=a1,tree[2]=a1+a2,tree[3]=a3,tree[4]=a4+a3+a2+a1,tree[5]=a5,tree[6]=a5+a6, .........。那为什么tree是这样赋值的呢?我们可以观察到
8=1000,此时tree[8]=a1+...+a8,7=111,tree[7]=a7,6=110,tree[6]=a5+a6, 可知,tree数组的赋值个数由下表决定。它由多少个数值相加取决于下标的二进制末尾有几个0,有k个0,就有2k个值相加。8末尾有三个0,就有2^3个值相加。那我们如何找到有几个0呢?实际上,我们可以转化为找到下标的最后一个1所在位置。这里有一个神奇的操作lowbit(x)=x&(-x),就能实现。
其原理是利用负数的补码表示,负数的补码是原码取反加一。例如x=6=00000110,-x=11111010,lowbit(x)=2
那我们如何求和呢?
sum[4]=tree[4]
sum[7]=tree[7]+tree[6]+tree[4]
sum[9]=tree[9]
根据图表我们很容易得到上述关系,但是如果不画图呢?可以观察到:如7
7=111
6=110
4=100
可以联系上文可知,从7开始,先加上tree[7],然后7-lowbit(7)=6,加上tree[6],接着6-lowbit(6)=4,加上tree[4],最后4-lowbit(4)=0,结束。
int sum(int x){ int s=0; while(x>0){ s+=tree[x]; x-=lowbit(x); } return s; }
tree数组的更新
更改ak,和它相关的tree[]都会改变。例如改变a3,那么tree[3]、、tree[4]、tree[8]等都会改变。同样这个计算也利用了lowbit(x)。
首先改变tree[3],然后3+lowbit(3)=4,更改tree[4],4+lowbit( 4 )=8,更改tree[8],知道最后的tree[n]。
void add(int x,int d){ while(x<=n){ tree[x]+=d; x+=lowbit(x); } }
例题:poj2182
题意:n头牛,身高为1-n,告诉你从第二只牛开始,告诉你前面有prei只牛比它矮,要你输出这个序列牛的身高。
分析:从最后一只牛开始,prei+1就是他的身高,每确定一只,就减去1,往前推。
#include <iostream> #include<cstdio> using namespace std; const int N=1e5; int tree[N],pre[N],ans[N]; int n; #define lowbit(x) ((x)&(-x)) void add(int x,int d){ while(x<=n){ tree[x]+=d; x+=lowbit(x); } } int sum(int x){ int s=0; while(x>0){ s+=tree[x]; x-=lowbit(x); } return s; } int findpos(int x){ int l=1,r=n; while(l<r){ int mid=(l+r)/2; if(sum(mid)<x) l=mid+1; else r=mid; } } int main(){ cin>>n; pre[1]=0; for(int i=2;i<=n;i++) scanf("%d",&pre[i]); for(int i=1;i<=n;i++) tree[i]=lowbit(i); for(int i=n;i>=1;i--){ int x=findpos(pre[i]+1); //cout<<x<<endl; add(x,-1); ans[i]=x; } for(int i=1;i<=n;i++) printf("%d\n",ans[i]); }