树状数组的学习

本文深入讲解了树状数组的基本概念及其实现原理,探讨了如何利用树状数组高效地完成区间求和等操作,并对比了树状数组与线段树的特点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

 

今天学习了一下树状数组,它的主要作用就是对于连续区间值和查找,虽然修改的复杂度为logN,但是求a[1]~a[n]的和的时间复杂度也为logN.相对于线段树来说,时间复杂度相同,而空间复杂度更低,而且编程难易度也较线段树简单,缺点就是适用范围没有线段树广.典型的例题可以看一下POJ的2352.

 

以下文章转自http://www.cnblogs.com/yykkciwei/archive/2009/05/08/1452889.html

 

 


 

先废话一下.

嗯 嗯  从今天开始 从基础一步一步开始 追逐我的梦想!

今天先来讨论一下树状数组.

问题提出:已知数组a[],元素个数为n,现在更改a中的元素,要求得新的a数组中i到j区间内的和(1<=i<=j<=n).

思考:对于这个问题,我们可以暴力地来解决,从a[i]一直累加到a[j],最坏的情况下复杂度为O(n),对于m次change&querry,合起来的复杂度为O(m*n),在n或m很大的情况下,这样的复杂度是让人无法忍受的.另外,如果没有元素的变更,我们完全可以存储sum[1,k](k=1,2,……),然后对任意给定的查找区间[i,j],都可以方便的用ans=sum[1,j]-sum[1,i-1],当然这只是没有元素改变的情况下的比较优化的解法.那么对于有元素变更的问题是否有更高效的方法呢?(废话!没有我还写啥?!)可以想一下,每次更改的元素是比较少的,有时候甚至每次只改变一个元素,但是在用暴力方法求区间和的时候,却对区间内所有的元素都累加了一遍,这样其实造成了许多无谓的运算.这时候也许会想到如果能把一些结果存起来会不会减少很多运算?答案是肯定的,但问题是怎么存,存什么?如果存任意区间的话,n比较大的时候不但内存吃不消,而且存储的量太大,不易更改,反而得不偿失;那么也许可以考虑存储特定的一些区间(比如说线段树,其实现在讨论的问题用线段树完全可以解,以后再详细写线段树).那么现在重新回过头来,看下这个问题,我们已经确定了要存储一些特定区间sum的想法,接下来我们要解决的无非是两个问题:1、减少更改元素后对这些区间里的sum值的更改时间.2、减少查找的时间.

好了废话了这么半天,无非是想让自己以及看到的人明白为什么要用树状数组.

接下来正式入题.

首先我们可以借鉴元素不变更问题的优化方法,先得到前i-1项之和and前j项之和,以s[i]表示前i项之和,那么sum[i,j]=s[j]-s[i-1].那么现在的问题已经转化为求前i项之和了.另外,我们已经确定要存储一些特定区间的和,现在就要来揭示这些特定的区间究竟指什么.

在文字说明之前先引入一个非常经典的,在网上找到的树状数组文章里几乎都要出现的一个图片

从图中不难发现,c[k]存储的实际上是从k开始向前数k的二进制表示中右边第一个1所代表的数字个元素的和(这么说可能有点拗口,令lowbit为k的二进制表示中右边第一个1所代表的数字,然后c[k]里存的就是从a[k]开始向前数lowbit个元素之和)这么存有什么好处呢?无论是树状数组还是线段树,都用到了分块的思想,而树状数组采用这样的存储结构我想最主要的还是这样方便计算,我们可以用位运算轻松地算出lowbit.分析一下这样做的复杂度:对于更改元素来说,如果第i个元素被修改了,因为我们最终还是要求和,所以可以直接在c数组里面进行相应的更改,如图中的例子,假设更改的元素是a[2],那么它影响到得c数组中的元素只有c[2],c[4],c[8],我们只需一层一层往上修改就可以了,这个过程的最坏的复杂度也不过O(logN);对于查找来说,如查找s[k],只需查找k的二进制表示中1的个数次就能得到最终结果,比如查找s[7],7的二进制表示中有3个1,也就是要查找3次,到底是不是呢,我们来看上图,s[7]=c[7]+c[6]+c[4],可能你还不知道怎么实现这个过程.

还以7为例,二进制为0111,右边第一个1出现在第0位上,也就是说要从a[7]开始向前数1个元素(只有a[7]),即c[7];

然后将这个1舍掉,得到6,二进制表示为0110,右边第一个1出现在第1位上,也就是说要从a[6]开始向前数2个元素(a[6],a[5]),即c[6];

然后舍掉用过的1,得到4,二进制表示为0100,右边第一个1出现在第2位上,也就是说要从a[4]开始向前数4个元素(a[4],a[3],a[2],a[1]),即c[4].

 

代码

  int lowbit(int x)//计算lowbit
{
    return x&(-x);
}

void add(int i,int val)//将第i个元素更改为val
{
    while(i<=n)
    {
        c[i]+=val;
        i+=lowbit(i);
    }

}

int sum(int i)//求前i项和
{
    int s=0;
    while(i>0)
    {
        s+=c[i];
        i-=lowbit(i);
    }

    return s;
}

 

 


 

 

以下文章转自http://dongxicheng.org/structure/binary_indexed_tree/

 


 

 

 

 

1、概述

树状数组(binary indexed tree),是一种设计新颖的数组结构,它能够高效地获取数组中连续n个数的和。概括说,树状数组通常用于解决以下问题:数组{a}中的元素可能不断地被修改,怎样才能快速地获取连续几个数的和?

2、树状数组基本操作

传统数组(共n个元素)的元素修改和连续元素求和的复杂度分别为O(1)和O(n)。树状数组通过将线性结构转换成伪树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改和求和复杂度均为O(lgn),大大提高了整体效率。

给定序列(数列)A,我们设一个数组C满足

C[i] = A[i–2^k+ 1] + … + A[i]

其中,k为i在二进制下末尾0的个数,i从1开始算!

则我们称C为树状数组。

下面的问题是,给定i,如何求2^k?

答案很简单:2k=i&(i^(i-1)) ,也就是i&(-i)

下面进行解释:

数组C的具体含义如下图所示:

当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(logn)。另外,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。

树状数组能快速求任意区间的和:A[i] + A[i+1] + … + A[j],设sum(k) = A[1]+A[2]+…+A[k],则A[i] + A[i+1] + … + A[j] = sum(j)-sum(j-1)。

下面给出树状数组的C语言实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//求2^k
 
int lowbit( int t)
 
{
 
     return t & ( t ^ ( t - 1 ) );
 
}
 
//求前n项和
 
int sum( int end)
 
{
 
    int sum = 0;
 
    while (end > 0)
 
   {
 
      sum += in[end];
 
      end -= lowbit(end);
 
   }
 
   return sum;
 
}
 
//增加某个元素的大小
 
void plus( int pos, int num)
 
{
 
    while (pos <= n)
 
   {
 
      in[pos] += num;
 
      pos += lowbit(pos);
 
   }
 
}

3、扩展——二维树状数组

一维树状数组很容易扩展到二维,二维树状数组如下所示:

C[x][y] = sum(A[i][j])

其中,x-lowbit[x]+1 <= i<=x且y-lowbit[y]+1 <= j <=y

4、应用

(1)    一维树状数组:

参见:http://hi.baidu.com/lilu03555/blog/item/4118f04429739580b3b7dc74.html

(2)    二维树状数组:

一个由数字构成的大矩阵,能进行两种操作

1) 对矩阵里的某个数加上一个整数(可正可负)

2) 查询某个子矩阵里所有数字的和

要求对每次查询,输出结果

5、总结

树状数组最初是在设计压缩算法时发现的(见参考资料1),现在也会经常用语维护子序列和。它与线段树(具体见:数据结构之线段树)比较在思想上类似,比线段树节省空间且编程复杂度低,但使用范围比线段树小(如查询每个区间最小值问题)。

6、参考资料

(1)    Binary Indexed Trees:

http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees

(2)    吴豪文章《树状数组》:

http://www.java3z.com/cwbwebhome/article/article19/zip/treearray.zip

(3)    郭炜文章《线段树和树状数组》:

http://poj.org/summerschool/1_interval_tree.pdf

 


 

 

 

 

 

<think>好的,我现在要帮用户查找关于树状数组学习资料或教程。用户已经提供了三个引用内容,我需要先仔细阅读这些引用,理解它们的关键点,然后结合自己的知识来整理回答。 首先看引用[1],里面提到树状数组的特点是编程简洁,使用lowbit技术,代码效率高,比线段树更高效。这里可能需要解释一下lowbit是什么,以及为什么代码效率更高。引用[2]则更详细地介绍了树状数组的定义,用途是处理范围查询和单点更新,比线段树快。而引用[3]说明了当修改数组元素时,前缀和需要重新计算,树状数组解决了这个问题。 用户的问题是需要学习树状数组的教程或详解,所以我的回答需要结构清晰,分步骤讲解,可能包括概念、原理、核心操作、代码示例以及应用场景。同时,根据用户提供的系统级指令,数学公式要用$...$,独立公式用$$...$$,并且生成相关问题。 接下来要确保回答符合中文,正确使用LaTeX格式。比如lowbit的公式应该是$lowbit(x) = x \& (-x)$,需要用行内公式格式。此外,可能需要给出树状数组的结构示意图,但由于文本限制,可能需要用文字描述结构。 然后需要组织内容:先介绍树状数组的基本概念,然后讲解lowbit的原理,接着说明核心操作(更新和查询),给出代码示例,再讨论应用场景,最后根据引用内容添加参考资料。同时,在回答末尾生成相关问题,帮助用户深入理解。 要注意引用标记,在引用内容后自然添加[^1]、[^2]等。例如在讲解lowbit时引用[1],在解释应用场景时引用[2]和[3]。 需要检查是否有遗漏的重要点,比如树状数组的优缺点,与其他数据结构(如线段树)的比较,这些可能对用户有帮助。同时,代码示例需要正确,可能用Python或C++,但用户提供的引用中有C++,所以可能更倾向用C++示例,但用户之前的示例是Python,所以可能需要兼顾或说明。 最后,生成的相关问题要围绕树状数组的深入理解、应用、比较等方面,比如与线段树的区别,时间复杂度,如何扩展到多维等。 现在需要把这些思考整理成一个连贯的回答,确保每个部分自然衔接,引用正确,格式符合要求。</think>### 树状数组详解 树状数组(Binary Indexed Tree, BIT)是一种高效处理**单点更新**和**前缀和查询**的数据结构,尤其适合动态维护数组的场景。其核心思想是通过二进制索引的树状结构,将时间复杂度优化到$O(\log n)$。 #### 1. 基本原理与结构 树状数组利用二进制中$lowbit$技术划分区间。定义$lowbit(x) = x \& (-x)$,例如$lowbit(6)=2$(6的二进制为$110$,取最低位的1)[^1]。 树状数组中每个节点$c[i]$管理原数组$arr$的区间为$[i-lowbit(i)+1, i]$,例如: - $c[4]$管理$arr[1-4]$(因为$lowbit(4)=4$) - $c[6]$管理$arr[5-6]$(因为$lowbit(6)=2$) #### 2. 核心操作 **(a) 单点更新** 当修改$arr[k]$时,需更新所有覆盖$k$的节点。例如更新$arr[3]$时,依次更新$c[3], c[4], c[8], \dots$ **代码示例(C++)**: ```cpp void update(int k, int val) { while (k <= n) { c[k] += val; k += lowbit(k); } } ``` **(b) 前缀和查询** 求$arr[1..k]$的和时,从$c[k]$开始向祖先跳跃累加。例如查询$k=6$时,累加$c[6], c[4]$ **代码示例(C++)**: ```cpp int query(int k) { int res = 0; while (k > 0) { res += c[k]; k -= lowbit(k); } return res; } ``` #### 3. 应用场景 - **动态维护逆序对**:通过离散化+树状数组统计右侧小于当前元素的个数[^3] - **区间求和与单点修改**:例如实时统计股票价格波动 - **替代线段树**:在一维且无需复杂区间操作时,代码更简洁[^1] #### 4. 与线段树的对比 | 特性 | 树状数组 | 线段树 | |--------------|---------------|---------------| | 代码复杂度 | 极低(10行内)| 较高(递归或非递归) | | 功能范围 | 单点更新+前缀和 | 支持区间修改/查询 | | 时间复杂度 | $O(\log n)$ | $O(\log n)$ |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值