树状数组原理及例题

简介

树状数组这个东西。。。有人说它像线段树。。。

其实感觉两者并没有什么直接联系。。。但是都是类似于树形操作的思想,所以经常把他俩放在一块说。。。

稍微讲讲我对这个算法的见解,大家看看对不对。。。

应用场景

可以用于需要对单个节点修改、对区间结果查询的情况。
需要注意的是,节点的信息必须可以被复合,即几个子节点的信息可以被融合到一个父级节点中,修改某个子节点可以通过更新父节点的方式维持这种融合关系。
最简单的一个例子,加和。

与前缀和的不同:前缀和适用于无更新的区间查询,树状数组适用于对区间进行单操作更新的情况。
与线段树的不同:线段树适用于多种区间操作更新的区间查询,并且有懒操作,虽然线段树与树状数组的复杂度相同,但是线段树结构更复杂,有多种懒操作机制,常数非常大。

基本思想

基本思想是这样的:

首先来讲,树状数组较好的利用了二进制。它的每个节点的值代表的是自己和前面一些元素的和。至于到底是前面哪些元素,这就由这个节点的下标决定。

比如下面这个图(图中的数字代表节点的下标):

树状数组

我们假设a[MAXN]数组用来存储初始数据,e[MAXN]代表了树状数组存储内容。

例如在上图的树状数组中,e[8]号记录了a[1]…a[8]的和,即e[4]+e[6]+e[7]+a[8]。绿色的线代表树的节点从哪些节点求和得到。

所以根据以上性质,树状数组实现的功能有:

  • 将一个数组转化成树状数组
  • 改变某一个点的值
  • 询问a[1]+a[2]+······+a[x]的值

具体原理

具体的就是二进制的原理,比较绕。。。先从数据结构开始讲。。。

数据结构

看图,这个图是上面那个图的所有树状数组节点编号变成二进制以后的样子:

树状数组二进制

你有木有发现什么蹊跷之处?(并木有发现)

树状数组的节点深度其实就是它的节点编号的二进制形式中,从右往左数第一个1出现的位置。(这谁能发现啊喂!)

比如说:

6的二进制形式中(110),从右往左数第一个1出现的位置是2(1 << 1 = 2)
7的二进制形式中(111),从右往左数第一个1出现的位置是1 (1 << 0 = 1)
8的二进制形式中(1000),从右往左数第一个1出现的位置是4(1 << 3 = 8)

那么我们定义一个函数,只留下那个 1,剩下的全部为 0,这样的函数称为 l o w b i t ( x ) lowbit(x) lowbit(x)函数。

即:

l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2
l o w b i t ( 7 ) = 1 lowbit(7)=1 lowbit(7)=1
l o w b i t ( 8 ) = 8 lowbit(8)=8 lowbit(8)=8

然后你还会发现,一个节点并不一定是代表自己前面所有元素的和。只有满足 2 n 2^n 2n这样的数才代表自己前面所有元素的和。

那么归纳一下,就能得出结论:

设节点的编号为 x x x,那么这个节点的值等于 a [ x − l o w b i t ( x ) + 1 ] a[x-lowbit(x)+1] a[xlowbit(x)+1] a [ x ] a[x] a[x] 的和。

比如说 e [ 6 ] = a [ 6 − 2 + 1 ] + ⋅ ⋅ ⋅ + a [ 6 ] = a [ 5 ] + a [ 6 ] e[6]=a[6-2+1]+ ··· + a[6]=a[5]+a[6] e[6]=a[62+1]++a[6]=a[5]+a[6]

再比如说 e [ 8 ] = a [ 8 − 8 + 1 ] + ⋅ ⋅ ⋅ + a [ 8 ] = a [ 1 ] + a [ 2 ] + ⋅ ⋅ ⋅ + a [ 8 ] = e [ 4 ] + e [ 6 ] + e [ 7 ] + a [ 8 ] e[8]=a[8-8+1]+···+a[8]=a[1] + a[2] + ··· + a[8] = e[4]+e[6]+e[7]+a[8] e[8]=a[88+1]++a[8]=a[1]+a[2]++a[8]=e[4]+e[6]+e[7]+a[8]

这就是树状数组比较神奇的地方之一。

lowbit函数的实现

那么,如何做到呢?这里先给出最终的 C++ 代码:

int lowBit(int& x)
{
	return x & ((~x) + 1);
}

因为这里保证调用时传入的是非负数,那么还可以再简化一下:

int lowBit(int& x)
{
    return x & (-x);
}

为什么?因为对于计算机来说,所有数都是以补码的形式存储的,非负数的补码和原码相同,负数的补码等于源码取反 + 1,所以这两种方式对于正数来说本质上是相同的。

lowbit函数证明

那么我们来证明一下。注意,这里的证明只是一种相对容易理解的通俗证明,并非严格的数学证明。

我们从右往左来看。我们来看第一位,假设 x 第一位(二进制形式下最右边的那位)是 1,记为 x[0] = 1,会出现下面的状况:

  • x[0] = 1
  • (~X)[0] = 0
  • (~X + 1)[0] = (~X)[0] + 1 = 1

如果 x[0] = 0,会出现下面的状况:

  • x[0] = 0
  • (~X)[0] = 1
  • (~X + 1)[0] = (~X)[0] + 1 = 0,需要给 (~X)[1] 进位,也就是 + 1

然后在 x[0] = 0 的情况下,如果 x[1] = 1,则会出现下面的情况:

  • x[1] = 1
  • (~X)[1] = 0
  • (~X + 1)[1] = (~X)[1] + 1 = 1

如果 x[1] = 0,则会出现下面的情况:

  • x[1] = 0
  • (~X)[1] = 1
  • (~X + 1)[1] = (~X)[1] + 1 = 0,需要给 (~X)[2] 进位,也就是 + 1

如此类推下去。你会发现,只要原数某一位是 1,那么在 取反 + 1 的操作中, +1 的影响范围就会止步于这一位。

那么我们对 x 进行了取反 + 1 操作以后,设从右往左看第一个出现的 1 在第 a 位上,那么,第 a 位右边的所有位都是 0,第 a 位是 1,第 a 位左侧都是 x 取反的结果。

举例:假设原数 x 是 8 位二进制数,它的二进制表示形式为:

1011 0100

那么取反 + 1后的结果是:

0100 1100

可以看到,第三位左侧都是原数取反,第三位右侧都是 0。

这时候,我们再让它和原数做一次按位与(AND)运算,由于左侧都是取反,那么 AND 之后左侧都是 0,而右侧由于本身全是 0,因此 AND 后也全是 0。

那么就得到了最终的答案:

0000 0100

这正好就是我们想要的答案。

修改一个元素的值

因为树状数组的特殊性质,我们只需要修改所有包含这个元素的节点就行了。

那怎么操作呢?

假设本次对 e [ x ] e[x] e[x]进行了操作,那么下一次就对它的上级节点,也就是 e [ x + l o w b i t ( x ) ] e[x+lowbit(x)] e[x+lowbit(x)]进行操作就行了。

论证的话,前面说过,lowbit 与节点所处的深度有关,比如一个深度为 4 (比如 8 号节点)的节点,它的 lowbit 就是 2 4 − 1 = 8 2^{4 - 1} = 8 241=8

观察一下上面的树形结构,一个节点的上级节点,它的编号都是 x + l o w b i t ( x ) x+lowbit(x) x+lowbit(x)

void add(int x,int v)
{
    while(x<=len)
    {
        e[x]+=v;
        x+=lowbit(x);
    }
}

查询

查询是为了得到 a [ 1 ] + a [ 2 ] + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + a [ x ] a[1]+a[2]+······+a[x] a[1]+a[2]++a[x]的值。

从前面的论证中,我们知道:

设节点的编号为 x x x,那么这个节点的值等于 a [ x − l o w b i t ( x ) + 1 ] a[x-lowbit(x)+1] a[xlowbit(x)+1] a [ x ] a[x] a[x] 的和。

那么查询的办法就很明了了,运用递归的思想,对于 a [ 1 ] + a [ 2 ] + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + a [ x ] a[1]+a[2]+······+a[x] a[1]+a[2]++a[x],先取得 a [ x − l o w b i t ( x ) + 1 ] a[x-lowbit(x)+1] a[xlowbit(x)+1] a [ x ] a[x] a[x] 的和,也就是 e[x],然后再取 a [ 1 ] + a [ 2 ] + ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ + a [ x − l o w b i t ( x ) ] a[1]+a[2]+······+a[x-lowbit(x)] a[1]+a[2]++a[xlowbit(x)] 相加即可,依次向下走。

可以得到每一次操作:

答案加上 x x x对应节点的值,然后将 x x x减去它的 l o w b i t lowbit lowbit,继续进行这样的操作,直到 x x x小于等于 0 0 0

不信。。。用图片验证一下吧。。。

int query(int x)
{
    int sum=0;
    while(x>0)
    {
        sum+=e[x];
        x-=lowbit(x);
    }
    return sum;
}

例题

树状数组1

洛谷-P3374【模板】树状数组1

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某一个数加上x

2.求出某区间每一个数的和

输入输出格式

输入格式:

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x k 含义:将第x个数加上k

操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和

输出格式:

输出包含若干行整数,即为所有操作2的结果。

标程

贴上我的标程。。。

其实也就是完整的树状数组。。。

#define MAXN 500005
class TA
{
    private:
        int e[MAXN];
        int len;
        int lb(int k)
        {
            return k&(-k);
        }
    public:
        void add(int x,int v)
        {
            while(x<=len)
            {
                e[x]+=v;
                x+=lb(x);
            }
        }
        void init(int* getin,int _len)
        {
            len=_len;
            for(int i=1;i<=len;i++)
            {
                add(i,*(getin+i-1));
            }
        }
        int query(int x)
        {
            int sum=0;
            while(x>0)
            {
                sum+=e[x];
                x-=lb(x);
            }
            return sum;
        }
};
TA ta;
int a[MAXN],n,m;

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    ta.init(a+1,n);
    for(int i=1;i<=m;i++)
    {
        int ope,a,b;
        scanf("%d%d%d",&ope,&a,&b);
        if(ope==1)
        {
            ta.add(a,b);
        }
        else
        {
            cout<<ta.query(b)-ta.query(a-1)<<endl;
        }
    }
    return 0;
}

树状数组2

洛谷-【模板】树状数组 2

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数数加上x

2.求出某一个数的和

输入输出格式

输入格式:

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含2或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k

操作2: 格式:2 x 含义:输出第x个数的值

输出格式:

输出包含若干行整数,即为所有操作2的结果。

标程

这个题不仅是树状数组,当然还使用了线段树的思想。

修改的时候,只需要将组成这个区间的几个节点加这个数(类似于线段树中的懒操作)。

查询的时候,依层找自己的上级,然后加上自己上级的值就行了。因为在这里,上级的值就相当于懒操作的值。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;

#define MAXN 500005

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

int e[MAXN];
int a[MAXN],n,m;

void addto(int x,int v)
{
    while(x>0)
    {
        e[x]+=v;
        x-=lb(x);
    }
}//实现1-x区间加v 

int query(int x)
{
    int ans=a[x];
    while(x<=n)
    {
        ans+=e[x];
        x+=lb(x);
    }
    return ans;
} 

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
    {
        int operate;
        scanf("%d",&operate);
        if(operate==1)
        {
            int a,b,v;
            scanf("%d%d%d",&a,&b,&v);
            addto(b,v);
            addto(a-1,-v);
        }
        else
        {
            int x;
            scanf("%d",&x);
            cout<<query(x)<<endl;
        }
    }
    return 0;
}

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值