数据结构1——树状数组

一、相关定义

树状数组

  • 获取数组中连续n个数的和
  • 修改数组中某点的值
  • 时间复杂度:O(logn)

小结:树状数组的强项在于对数组进行维护查询(如,修改某点的值、求某个区间的和)。当然,数据规模不大的时候,对于修改某点的值是非常容易的,复杂度是O(1),但是对于求一个区间的和就要扫一遍了,复杂度是O(N),如果实时的对数组进行M次修改或求和,最坏的情况下复杂度是O(M*N),当规模增大后这是划不来的!而树状数组干同样的事复杂度却是O(M*logN)。

二、算法描述

列出即将出现的知识定义:

  • lowbit(k):把k的二进制的高位1全部清空,只留下最低位的1(即只能剩下一个相对位数最低的1)
  • 求解lowbit(k):lowbit(k)=k&-k
  • lowbit(k)的作用:联系数组a和数组c
  • ck:从ak开始(包括ak)往左连续求lowbit(k)个数的和
  • 去尾:把尾部应该去掉的1都去掉转而换到更高位的1,记住每次变换都要有一个高位的1产生
  • 去尾实现:k += lowbit(k)

配图如下(左边为图A,右边为图B):

【建立关系】

  图A是不是很像一颗树?对,这就是为什么叫树状数组了。A图中,a数组就是我们要维护和查询的数组,但是其实我们整个过程中根本用不到a数组,你可以把它当作一个摆设!c数组才是我们全程关心和操纵的重心。先由图来看看c数组的规则,其中c8=c4+c6+c7+a8,c6=c5+a6……先不必纠结怎么做到的,我们只要知道c数组的大致规则即可,很容易知道c8表示a1~a8的和,但是c6却是表示a5~a6的和,为什么会产生这样的区别的呢?或者说发明它的人为什么这样区别对待呢?答案是,这样会使操作更简单!看到这相信有些人就有些感觉了,为什么复杂度被log了呢?可以看到,c8可以看作a1~a8的左半边和+右半边和,而其中左半边和是确定的c4,右半边其实也是同样的规则把a5~a8一分为二……继续下去都是一分为二直到不能分,可以看看B图。怎么样?是不是有点二分的味道了?对,说白了树状数组就是巧妙的利用了二分,它并不神秘,关键是它的巧妙!

它又是怎样做到不断的一分为二呢?说这个之前我先说个叫lowbit的东西,lowbit(k)就是把k的二进制的高位1全部清空,只留下最低位的1,比如10的二进制是1010,则lowbit(10)=lowbit(1010)=(0010)2 。介于这个lowbit在下面会经常用到,这里给一个非常方便的实现方式,比较普遍的方法lowbit(k)=k&-k,这是位运算,我们知道一个数加一个负号是把这个数的二进制取反+1,如-10的二进制就是-1010=0101+1=0110,然后用1010&0110,答案就是0010了!明白了求解lowbit的方法就可以了,继续下面。介于下面讨论十进制已经没有意义(这个世界本来就是二进制的,人非要主观的构建一个十进制),下面所有的数没有特别说明都当作二进制。

上面那么多文字说lowbit,还没说它的用处呢,它就是为了联系a数组和c数组的!ck表示从ak开始往左连续求lowbit(k)个数的和。比如c[0110]=a[0110]+a[0101],就是从0110开始(包括0110)往左计算了lowbit(0110)个数的和,(因为lowbit(0110)=0010)即从0110开始(包括0110)往左计算了0010(也就是2)个数的和,这0010个数分别为a[0110]与a[0101]。因为lowbit(0110)=0010,可以看到其实只有低位的1起作用,因为很显然可以写出c[0010]=a[0010]+a[0001],这就为什么我们任何数都只关心它的lowbit,因为高位不起作用(基于我们的二分规则它必须如此!),除非除了高位其余位都是0,这时本身就是lowbit。

【更新位置数据】

  既然关系建立好了,看看如何实现a某一个位置数据更改的,它不会直接改的(开始就说了,a根本不存在),它每次改其实都要维护c数组应有的性质,因为后面求和要用到。而维护也很简单,比如更改了a[0011],我们接着要修改c[0011],c[0100],c[1000],这是很容易从图上看出来的,但是你可能会问,他们之间有什么必然联系吗?每次求解总不能总要拿图来看吧?其实从0011——>0100——>1000的变化都是进行“去尾”操作,就是把尾部应该去掉的1都去掉、转而换到更高位的1,记住每次变换都要有一个高位的1产生,所以0100是不能变换到0101的,因为没有新的高位1产生,这个变换过程恰好是可以借助我们的lowbit进行的,k += lowbit(k)。

       好吧,现在更新的次序都有了,可能又会产生新的疑问了:为什么它非要是这种关系啊?这就要追究到之前我们说c8可以看作a1~a8的左半边和+右半边和……的内容了,为什么c[0011]会影响到c[0100]而不会影响到c[0101],这就是之前说的c[0100]的求解实际上是这样分段的区间 c[0001]~c[0001] 和区间c[0011]~c[0011]的和,数字太小,可能这样不太理解,在比如c[0100]会影响c[1000],为什么呢?因为c[1000]可以看作0001~0100的和加上0101~1000的和,但是0101位置的数变化并会直接作用于c[1000],因为它的尾部1不能一下在跳两级在产生两次高位1,是通过c[0110]间接影响的,但是,c[0100]却可以跳一级产生一次高位1。

可能上面说的你比较绕了,那么此时你只需注意:c的构成性质(其实是分组性质)决定了c[0011]只会直接影响c[0100],而c[0100]只会直接影响[1000],而下表之间的关系恰好是也必须是k +=lowbit(k)。

此时我们就是写出更新维护树的代码:

void add(int k,int num)
{
	while(k<=n)
	{
		tree[k]+=num;
		k += k&-k;    //k&-k 就是lowbit(k)
	}
}

有了上面的基础,说求和就比较简单了。

比如求0001~0110的和就直接c[0100]+c[0110],分析方法与上面的恰好逆过来,而且写法也是逆过来的,具体就不赘述了:

int read(int k)  //1~k的区间和
{
	int sum=0;
	while(k)
	{
		sum += tree[k];
		k -= k&-k;
	}
	return sum;
}

三、博客小结

  1. 树状数组说白了是按照二分对数组进行分组;
  2. 维护和查询都是O(logn)的复杂度;
  3. lowbit这里只是一个技巧,关键在于明白c数组的构成规律;
  4. 分析的过程二进制一定要深入人心,当作心目中的十进制
时间: 2024-10-03 05:16:27

数据结构1——树状数组的相关文章

数据结构:树状数组

关于树状数组的概述,可以看一下这篇博客:http://blog.csdn.net/int64ago/article/details/7429868 树状数组是一个可以高效地进行区间统计的数据结构,在思想上类似于线段树,比线段树节省空间,编程复杂度比线段树低,但适用范围比线段树小.主要工作也是查询和更新. 例题:POJ - 2352    (http://poj.org/problem?id=2352) 题目大意:输入n个星星坐标,坐标按y递增顺序输入,y相同按x递增顺序输入.定义一个星星的级别是

【数据结构之树状数组】从零认识树状数组

一.关于树状数组 树状数组(Binary Indexed Tree,简称BIT),是一种修改和查询复杂度都为O(logN)的数据结构.但树状数组仅支持单点修改,在查询时,树状数组也要求被查询的区间具有可区间加减的性质.不过,树状数组由于代码实现容易.占用空间小,常用于代替线段树. 二.详解树状数组 在这里,我们定义原序列为a,树状数组为c,则有: 其中,k为i的二进制表示中末尾0的个数,例如:i=3(101)时,k=0:i=8(1000)时,k=3. 定义函数lowbit(x)=2k(k为x的二

『数据结构』树状数组

树状数组的问题模型: 现在有一个这样的问题: 有一个数组\(a\),下标从\(0\)到\(n-1\),现在你要进行\(w\)次修改,\(q\)次查询. 修改是修改数组中某一个元素的值: 查询是查询数组中任意一个区间的和,\(w+q<500000\). 这个问题很普遍,首先分析下朴素做法的时间复杂度, 修改是\(O(1)\)的时间复杂度, 而查询就要\(O(n^2)\)的复杂度,总体时间复杂度为\(O(q*q*n*n)\): 你或许会想到用前缀和来优化这个查询, 我们再来分析下,查询的话是\(O(

数据结构之树状数组

树状数组适合单个元素经常修改,而且还要反复求某个区间的和 树状数组的编程效率和程序运行效率都要比线段树要高(时间复杂度一样,但是梳妆数组的常数较小) 如果每次修改的不是一个数,而是一个区间就不适合用树状数组了(效率较低) 树状数组的时间复杂度总结: 建数组0(n) 更新0(logn) 局部求和0(logn) 当想要查询一个SUM(n)(求a[n]的和),可以依据如下算法即可: step1: 令sum = 0,转第二步: step2: 假如n <= 0,算法结束,返回sum值,否则sum = su

【数据结构】树状数组

树状数组 ta的本质是利用二进制的性质维护一组数据 最常用的操作就是求前缀和 int lowbit(int x){ return x&(-x); /*通过补码,清空高位1,只留下最后一个1*/ } void add(int x,int val){ while(x<=n){ c[x]+=val; x+=lowbit(x); } /*更新时,需要去把该节点所被管辖的结点全部更新 你比如说1101->1110 所被管辖的结点 1110->10000 被管辖的结点,是利用二进制来求的 最

数据结构(树状数组):HEOI2012 采花

[题目描述] 萧薰儿是古国的公主,平时的一大爱好是采花. 今天天气晴朗,阳光明媚,公主清晨便去了皇宫中新建的花园采花.花园足够大,容纳了n朵花,花有c种颜色(用整数1-c表示),且花是排成一排的,以便于公主采花.公主每次采花后会统计采到的花的颜色数,颜色数越多她会越高兴!同时,她有一癖好,她不允许最后自己采到的花中,某一颜色的花只有一朵.为此,公主每采一朵花,要么此前已采到此颜色的花,要么有相当正确的直觉告诉她,她必能再次采到此颜色的花.由于时间关系,公主只能走过花园连续的一段进行采花,便让女仆

浅谈树状数组

还是区间求和区间修改的问题,我们使用线段树解决以后发现编程复杂度比较大 在这里介绍一个简单的数据结构,树状数组. 树状数组的优势是编程复杂度小,常数小,时间复杂度也不错 树状数组的查询,修改,都是LOG(N)级别的 下面来分析一下上面那个图看能得出什么规律: 据图可知:c1=a1,c2=a1+a2,c3=a3,c4=a1+a2+a3+a4,c5=a5,c6=a5+a6,c7=a7,c8=a1+a2+a3+a4+a5+a6+a7+a8,c9=a9,c10=a9+a10,c11=a11.......

树状数组小结

树状数组基本概念 树状数组也是进行区间操作的常用数据结构.树状数组适用于单个元素经常修改,而且还反复求部分的区间和的情况. 对于数组a,构造一个新的数组C,使得C[i] = a[i-2^k+1] + a[i-2^k+2] + ... + a[i]; (1) i >= 1: (2) k为i在二进制表示下末尾的连续的0的个数,2^k = i&(-i),通常用lowbit(i)表示i对应的2^k,lowbit(i) = 2^k = i&(-i): 则数组C为数组a的树状数组. 数组数组的结

树状数组详解(图形学算法)

目录 一.从图形学算法说起 1.Median Filter 概述 2.r pixel-Median Filter 算法 3.一维模型 4.数据结构的设计 5.树状数组华丽登场 二.细说树状数组 1.树 or 数组? 2.结点的含义 3.求和操作 4.更新操作 5.lowbit函数O(1)实现 6.小结 三.树状数组的经典模型 1.PUIQ模型 2.IUPQ模型 3.逆序模型 4.二分模型 5.再说Median Filter 6.多维树状数组模型 四.树状数组题集整理 一.从图形学算法说起 1.M