树状数组

引言

  在解题过程中,我们有时需要维护一个数组的前缀和S[i]=A[i]+A[2]+...+A[i].但是不难发现,如果我们修改了任意一个A[i],S[i],S[i+1]...S[n]都会发生变化。可以说,每次修改A[i]后,调整前缀和S在最坏的情况下会需要O(n)的时间。当n非常大时,程序会运行得非常慢。因此,这里我们引入“树状数组”,它的修改与求和都是O(logn),效率非常高。

基本思想

  根据任意正整数关于2的不重复次幂的唯一分解性质,若一个正整数x的二进制表示为10101,其中等于1的位是0,2,4,则正整数x可以被"二进制分解成2^4+2^2+2^0"。进一步地,区间[1,x]可以分成O(logx)个小区间:

  1.长度为2^4的小区间[1,2^4]

  2.长度为2^2的小区间[2^4+1,2^4+2^2]

  3.长度为2^0的小区间[2^4+2^2+1,2^4+2^2+2^0]

  树状数组就是一种基于上述思想的数据结构,其基本用途是维护序列的前缀和。对于区间[1,x],树状数组将其分为logx个子区间,从而满足快速询问区间和。

基本算法

  由上文可知,这些子区间的共同特点是:若区间结尾为R,则区间长度就等于R的“二进制分解”下最小的2次幂,我们设为lowbit(R)。

  对于给定的序列A,我们建立一个数组c,其中c[x]保存序列A的区间[x-lowbit(x)+1,x]中所有数的和。

  事实上,数组c可以看做一个如下图所示的树形结构。

                                              

 

该结构满足以下性质:

 1.每个内部结点c[x]保存以它为根的子数中所有叶节点的和

 2.每个内部结点c[x]的子节点个数等于lowbit(x)的大小

 3.除树根外,每个内部结点c[x]的父结点c[x+lowbit(x)]

 4.数的深度为O(logN)

求lowbit(x)

 lowbit(n)表示取出非负整数n在二进制表示下最低位的1以及它后边的0构成的数值。

lowbit(n)=n&(-n),这是怎么得来的?具体分析如下:

 设n>0,n的第K位是1,第0~k-1位都是0.

 为了实现lowbit运算,先把n取反,此时第k位变为0,第0~k-1位都是1.再令n=n+1,此时因为进位,第k位变为1,第0~k-1位都是0.在上面的取反操作后,n的第k+1到最高位恰好与原来相反,所以n&(~n+1)仅有第k位为1,其余位为0.而在补码表示下,~n=-1-n,因此lowbit(n)=n&(-n).

代码实现

int lowbit(int n)
{ 
   return n&(-n);
}

对某个元素进行加法操作

 树状数组支持单点增加,意思是给序列中的某个数A[x]加上y,同时正确维护序列的前缀和。

根据上面给出的树形结构和它的性质,只有结点c[x]及其所有祖先结点保存的"区间和"包含A[x],而任意一个结点的祖先至多只有logN个,我们逐一对它们的数值c值进行更新即可。下面的代码在O(logN)时间内执行单点增加操作。

代码实现

 

void update(int x,int y)
{
   for(;x<=N;x+=x&-x)
      c[x]+=y;
}

查询前缀和

 树状数组支持查询前缀和,即序列A第1~x个数的和。按照我们刚才提出方法,应该求出x的二进制表示中每个等于1的位。把[1,x]分成O(logN)个小区间,而每个小区间的区间和都已经保存在数组c中。下面的代码是在O(logN)时间内查询前缀和:

int sum(int x)
{
   int ans=0;
   for(;x;x-=x&-x)
      ans+=c[x];
   return ans;
}

统计A[x]....A[y]的值

  调用以上的sum操作:sum(y)-sum(x-1)

扩展(多维数组)

  一维的树状数组的每个操作的复杂度都是O(logn)的,非常高效。它可以扩充为m维这样每个操作的复杂度就变成了O(log^mn),在m不大的时候,时间复杂度是可以接受的。扩充的方法就是将原来的修改和查询函数中的一个循环,改成m个循环m维数组c中的操作。也就是说,如果有n*m的二维数组a,树状数组为c,那么单点修改操作为:

int update(int x,int y,int z)
{
    int i=x;
    while(i<=n)
    {
        int j=y;
        while(j<=m)
        {
            c[i][j]+=z;
            j+=lowbit(j);
        }
        i+=lobit(i);
    }
}
int sum(int x,int y)
{
    int re=0,i=x;
    while(i>0)
    {
        int j=y;
        while(j>0)
        {
            re+=c[i][j];
            j-=lowbit(j);
        }
        i-=lowbit(i);
    }
    return re;
}

注意事项

要注意树状数组能处理的是下标为1...n的数组,绝不能出现下边为0的情况。因为lowbit(0)=0,这样会陷入死循环。

                                                                                                                               摘自《信息学奥赛一本通 提高篇》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值