您的位置:首页 > 编程语言 > C语言/C++

树状数组 Binary Indexed Tree

2016-06-29 00:21 316 查看

树状数组 (Binary Indexed Tree)

树状数组 Binary Indexed Tree
引入

问题形式

基本思想

实现
子集和划分

查询前缀和

修改子集和

常用技巧
查询任意区间和

利用sumsum数组求aia_i

成倍扩大缩小

扩展到高维

例题
逆序对

引入

在一些数列问题中,使用一般的算法时间复杂度太高,空间复杂度又无法让人接受,那么此时树状数组将会是一个很好的选择。另外,树状数组能解的问题线段树也能解,而线段树能解的问题树状数组不一定能解,但线段树和树状数组都可解的问题中,树状数组往往比线段树更加好写,时间复杂度往往会更低一些。

问题形式

定义一个数组a[n],一般树状数组可实现一下两种操作

1、修改操作:给a[i]增加一个增量delta

2、查询操作:询问a[1...index]的前缀和,即∑indexi=1a[i]

朴素的算法能在O(1)完成修改,但是查询需要O(n),而采用树状数组,则能解决较多次查询的问题

基本思想

对于每次求前缀和,我们将其分解为一系列的子集,进而进行求和,但是要注意,分解出来的子集不能相交。例如,将15可以分解为23+22+21+20,那么类似的,可以将∑indexi=1a[i]也分解为几个子集的和。那么怎么分呢?一般来说,如果index的二进制表示中有k个1,那么我们就将其分解为k个子集的和,所以,我们可以给出以下的子集划分:

下标二进制子集
111
2101~2
3113
41001~4
51015
61105~6
71117
810001~8
910019
其中,下标代表了每个子集的编号,子集即代表包含的a中的元素。比如说,下标为8,那么这个子集里就包含了∑8i=1a[i],而下标为3的只包含了a[3]。然而为啥要这样划分呢?来看一个表:

下标1234567891011!2
a数组201102301021
前缀和2234469910101213
子集和sum221402391124
如图:

子集和查询树
如图所示:

图中的每一个长方形都代表每个子集对应的部分和,深色代表了自己下标所对应的值a[i],浅色代表还需要维护的别的值a[k...i−1]。
对于每个查询,我们如果顺着黑色的实线依次走下去,就可以得到对应的前缀和了。如图:

如此一来,对于每个查询,我们找到树中对应标号的结点,顺着边一直走到0,依次累加对应的子集和,到根的时候,我们就得到了最终的前缀和。通过观察我们还能发现:每个结点的深度代表了对应查询所需要累加子集和的数量,也就是对应数字的二进制中1的个数。另外,除了那个虚拟的根结点0之外,其他的每个结点的孩子个数都等于其二进制中末尾0的个数。因此我们叫它树状数组也叫Binary Indexed Tree

实现

子集和划分

下标12345678
二进制110111001011101111000
元素个数C12141218
C的二进制表示110110011011000
通过观察我们发现,元素个数C的二进制表示就是对应下标的二进制表示中最低位的1所在的位置对应的数!求这个C运用了低位技术(Lowbit),基于位运算,我们便能得到强大的lowbit函数。那么如何快速根据下标求得这个C呢?

不难得到求法:

方法1:C(i)=i−(iAnd(i−1))

方法2:C(i)=iAnd(−i)

(根据计算机中的补码思想,想想为什么)

查询前缀和

int C(int i){ //lowbit
return i&-i;
}
int query(int i){
int ans=0;
while (i>0) {
ans+=sum[i];
i-=C(i);
return ans;
}


时间复杂度O(logn)

修改子集和

当要修改一个元素a[i](给其增加delta)的时候,注意要将其所有的父节点的sum值都更新。

void change(int i,int delta){
while (i<=n){
sum[i]+=delta;
i=i+C(i);
}
}


常用技巧

查询任意区间和:

查询∑pi=ka[i]时,只需要计算query(p)−query(k−1)即可

利用sum数组求a[i]

要计算a[i],只需要计算query(i)−query(i−1),进一步,从查询树中,我们可以发现:a[i]=sum[i]−(query(i−1)−query(LCA(i,i−1)))

其中LCA为最近公共祖先。

int getval(int i){
int ans=sum[i];
int lca=i-C(i);
i--;
while (i!=lca) { ans-=sum[i]; i-=C(i);}
return ans;
}


成倍扩大/缩小

如何让a中所有元素变为原来的一半呢?如果直接对树状数组中所有的值除以2,由于取整的原因会造成不连续,但我们如果用倒序修改的方式就能完美解决此问题:

void enlarge(int x){
for (int i=n;i>=1;i--) change(i,-getval(i)/2);
}


扩展到高维

我们先来看一下二维的树状数组:

定义一个二维数组a[n][n],维护两种操作:

1、修改:给a[i][j]加一个delta;

2、查询:求∑xi=1∑yj=1a[i][j]

那么便可以将sum定义为sum[x][y]=∑xi=x−C(x)+1∑yj=y−C(y)+1a[i][j]

那么代码就很简单了:

int query2d(int x,int y){
int ans=0;
while (x>0) {
int ty=y;
while (ty>0){
ans+=sum[x][ty];
ty-=C(ty);
}
x-=C(x);
}
return ans;
}
void change2d(int x,int y,int delta){
while (x<=n){
int ty=y;
while (ty<=n){
sum[x][ty]+=delta;
ty+=C(ty);
}
x+=C(x);
}
}


同理,我们也可以扩展到n维,笔者在这里就不详细写了。

例题

逆序对

求数组a[n]中逆序对的数量,逆序对(i,j)满足1<=i<j<=n,a[i]>a[j]。

输入:第一行一个正整数n,代表数字个数;接下来一行有n个整数,代表数组的元素

输出:一个整数,代表逆序对个数

示例代码:

#include<iostream>

using namespace std;

typedef long long ll;

struct node {
ll v;
int d;
node(){
v=d=0;
}
};

int n;
node a[100001];
int sum[100001];
int c[100001];

inline int lb(int x){ //lowbit
return x&(-x);
}

void qs(int l,int r){ //快速排序
int i=l,j=r,mid=a[(l+r)/2].v;
do {
while (a[i].v<mid) i++;
while (a[j].v>mid) j--;
if (i<=j){
node mm=a[i];
a[i]=a[j];
a[j]=mm;
i++,j--;
}
}while (i<=j);
if (i>l) qs(i,r);
if (j<r) qs(l,j);
}

ll query(int x){
ll ans=0;
while (x>0){
ans+=sum[x];
x-=lb(x);
}
return ans;
}

void insert(int x){
while (x<=n){
sum[x]++;
x+=lb(x);
}
return ;
}

int main(){
ios::sync_with_stdio(false); //此句话可加快iostream的速度
cin>>n;
for (int i=1;i<=n;i++){
cin>>a[i].v;
a[i].d=i;
}
qs(1,n);
int last=-1,temp=-1,k=0;
for (int i=1;i<=n;i++){
temp=a[i].v;
if (temp!=last){
last=temp;
k++;
}
c[a[i].d]=k;
}
ll s=0;
for (int i=n;i>=1;i--){
insert(c[i]);
s+=query(c[i]-1);
}
cout<<s;
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  树状数组 算法 OI C++