树状数组

树状数组

特点

代码短、常数很小


应用及时间复杂度

区间查询:求前缀和
单点修改:给某个位置上的数加上一个数(同时能以非常小的代价维护前缀和)
时间复杂度:O(logn)
与一般前缀和算法的对比

算法 修改某个点 查询前缀和 平均时间复杂度(假定两种操作各占50%)

  1. 一般前缀和 O(n)
  2. 树状数组 O(logn)

可以看出在修改和查询操作占比差不多时,树状数组的效率更高

那么什么时候用树状数组,什么时候用一般前缀和算法呢?

这就要明白这两个算法的本质区别:
1. 一般前缀和算法是离线算法,它不支持动态的改变单个元素的值,或者说改变单个元素值后,重新维护前缀和所花费的代价很大。
2. 树状数组是在线算法,支持动态改变单个元素的值,以很小的代价动态维护前缀和。

所以当仅仅需要用到前缀和,不涉及动态的改变单个元素的值时,首选一般前缀和算法,否则就用树状数组。

树状数组原理 树状数组图例(下标从1开始)

假设原序列为a,树状数组序列为c,那么是怎么由原序列得到树状数组序列的呢?(可以把c理解为aa的前缀和序列,只是前缀和关系不像一般前缀和那样简单、线性)
首先,将一维的树状数组序列c看成多层的序列,c[i]属于第几层,取决于ii的二进制表示中最后一个1后面有几个0,有几个0就在第几层,显而易见,当i为奇数时,c[i]是在第0层的 因为lowbit(x)=2k,k表示x的二进制表示后面有多少个0 (lowbit(n)求得n的二进制表示中最后一个1以及往后的0)

可以得到关系:
c[x]=a(x−lowbit(x),x] 此关系描述了序列c中每个元素是哪一段序列a中元素的和

如何通过树状数组求前缀和?
由上面公式知道,想要求序列aa中11到xx的和,则应该是: ∑xi=1ai=c[x]+c[x−lowbit(x)]+......

因而可得代码:

int res=0;
for(int i=x;i>0;i-=lowbit(x)) res+=c[i];
return res;

如何通过树状数组进行单点修改?
这里我们给出一个结论:一个结点a[i]或c[i]的父结点为c[i+lowbit(i)]
所以当我们改变a[i]的值时,依次递归向上更新父结点的值即可。

代码:

a[x]+=v;
for(int i=x;i<=n;i+=lowbit(i)) c[i]+=v;

我们发现这里是给某个数加上一个数v,而不是把某个数变成v,如果想实现这样的效果,该怎么做呢?

我们可以把加的数vv灵活的调整为v−x(假设原来的数为xx),加上v−x之后原来的数x就变成vv了,从而实现了让一个数变成一个给定的数的效果。

获得原来的数的方式:
1. 树状数组前缀和相减:c[x]−c[x−1]
2. 开一个数组存原数组

树状数组只能给一个数加上一个数,而不能把一个数变成一个数,要实现这样的操作,作上面的变换即可。

初始化树状数组
可以假设原序列aa为全0,依次通过“单点修改”操作把每个数加进去,最后就可以形成树状数组了。


例题

AcWing 1264. 动态求连续区间和

#include
using namespace std;
const int N=100010;
int a[N],c[N];
int n,m;
int k,x,y;
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int v)
{
    for(int i=x;i<=n;i+=lowbit(i)) c[i]+=v;
}
int query(int x)
{
    int res=0;
    for(int i=x;i>0;i-=lowbit(i)) res+=c[i];
    return res;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) add(i,a[i]);   //初始化

    while(m--)
    {
        scanf("%d%d%d",&k,&x,&y);
        if(k==0) printf("%d\n",query(y)-query(x-1));
        else add(x,y);
    }
    return 0;
}

你可能感兴趣的:(算法,数据结构,c++)