树状数组---原理代码实现

刚刚学了树状数组,有必要总结一下;

(因为有人说;别人能很快理解算法,最好是让刚刚理解的人来教而不是研究多年的大牛,因为刚刚理解的才知道初学的人哪里不会;;当然这也不一样啦~~~)


或许你还不知道什么是树状数组,这里大致讲一下(参考wiki,baidu);


树状数组(Binary Indexed Tree,BIT,二分索引树),最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和。它可以以O(\log n)的时间得到\sum_{i=1}^N a[i],并同样以O(\log n)对某项加一个常数。

用途:主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值。

与线段树的比较:这种数据结构(算法)并没有C++和Java的库支持,需要自己手动实现。在Competitive Programming的竞赛中被广泛的使用。树状数组和线段树很像,但能用树状数组解决的问题,基本上都能用线段树解决,而线段树能解决的树状数组不一定能解决。相比较而言,树状数组效率要高很多。


看了这么多概念性的,相信你也烦了- -;不过这个确实需要了解;等你会了之后再看这个就一目了然为什么是如上面这样说的



简介

我们常常需要某种特定的数据结构来使我们的算法更快,于是乎这篇文章诞生了。 在这篇文章中,我们将讨论一种有用的数据结构:数状数组(Binary Indexed Trees)。 按 Peter M. Fenwich (链接是他的论文,TopCoder上的链接已坏)的说法,这种结构最早是用于数据压缩的。 现在它常常被用于存储频率及操作累积频率表。(都是废话,可以不用看了)

(重点操作)定义问题如下:我们有n个盒子,可能的操作为:

  1. 往第i个盒子增加石子(对应下文的update函数)
  2. 计算第k个盒子到第l个盒子的石子数量(包含第k个和第l个)

原始的解决方案中(即用普通的数组进行存储,box[i]存储第i个盒子装的石子数), 操作1和操作2的时间复杂度分别是O(1)和O(n)。假如我们进行m次操作,最坏情况下, 即全为第2种操作,时间复杂度为O(n*m)。使用某些数据结构(如 RMQ) ,最坏情况下的时间复杂度仅为O(m log n),比使用普通数组为快许多。 另一种方法是使用数状数组,它在最坏情况下的时间复杂度也为O(m log n),但比起RMQ, 它更容易编程实现,并且所需内存空间更少。

符号含义

  • BIT: 树状数组
  • MaxVal: 具有非0频率值的数组最大索引,其实就是问题规模或数组大小n
  • f[i]: 索引为i的频率值,即原始数组中第i个值。i=1…MaxVal
  • c[i]: 索引为i的累积频率值,c[i]=f[1]+f[2]+…+f[i]                                  注意这些符号的含义待会讲的时候才不会乱当然我后面会不断说明其代表含义
  • tree[i]: 索引为i的BIT值(下文会介绍它的定义)
  • num^- : 整数num的补,即在num的二进制表示中,0换为1,1换成0。如:num=10101,则 num^- =01010

注意: 一般情况下,我们令f[0]=c[0]=tree[0]=0,所以各数组的索引都从1开始。 这样会给编程带来许多方便。

注意:这里累积频率你可以理解为前n项和


基本思想

每个整数都能表示为一些2的幂次方的和,比如13,其二进制表示为1101,所以它能表示为: 13 = 20 + 22 + 23 .类似的,累积频率c[]可表示为其子集合之和。在本文的例子中, 每个子集合包含一些连续的频率值,各子集合间交集为空。比如累积频率c[13]= f[1]+f[2]+…+f[13],可表示为三个子集合之和(这里说的三个是随便举例的, 下面的划分也是随便举例的),c[13]=s1+s2+s3, 其中s1=f[1]+f[2]+…+f[4],s2=f[5]+f[6]+…+f[12],s3=f[13]。

idx记为BIT索引r记为idx的二进制表示中最右边的1后面0的个数(这个r后面还会出现记得它的含义), 比如idx=1100(即十进制的12),那么r=2。tree[idx]记为f数组中, 索引从(idx-2r +1)到idx的所有数的和,包含f[idx-2r +1]和f[idx]。即: tree[idx]=f[idx-2r +1]+…+f[idx],对照表1.1和1.2理解和推算一下,你就会一目了然。 我们也可称idx对索引(idx-2r +1)到索引idx负责(比如表1.2的 8管着1-8,9管着9)。(We also write that idx is responsible for indexes from (idx-2r +1)to idx)

()

假设我们要得到索引为13的累积频率(即c[13]),在二进制表示中,13=1101。因此(你看一下上面那个图), 我们是不是可以这样计算:c[1101]=tree[1101]+tree[1100]+tree[1000]  (c[13]=tree[13](管着f[13])+tree[12](管着f[9]~f[12])+tree[8](管着f[1]~f[8])),后面将详细讲解。


你先观察上面这个式子 c[1101]=tree[1101]+tree[1100]+tree[1000]    有没有发现tree[ ]有上面规律?


分离出最后的1

这里先给出上面那个问题的规律;每次分离出最后的一 不知道聪明的你有么有发现

注意: 最后的1表示一个整数的二进制表示中,从左向右数最后的那个1。


由于我们经常需要将一个二进制数的最后的1提取出来,因此采用一种高效的方式来做这件 事是十分有必要的。令num是我们要操作的整数。在二进制表示中,num可以记为a1b  (比如什么 101101(a) 1(1) 1001(b)这个是废话其实可以不用看), a代表最后的1前面的二进制数码,由于a1b中的1代表的是从左向右的最后一个1, 因此b全为0,当然b也可以不存在。比如说13=1101,这里最后的1右边没有0,所以b不存在。(都是废话)


我们知道,对一个数取负等价于对该数的二进制表示取反加1

所以-num等于(a1b)^- +1= a^- 0b^- +1。由于b全是0,所以b^- 全为1。最后,我们得到:

-num=(a1b)^- +1=a^- 0b^- +1=a^- 0(1…1)+1=a^- 1(0…0)=a^- 1b

现在,我们可以通过与操作(在C++,java中符号为&)将num中最后的1分离出来:

num & -num = a1b & a^- 1b = (0…0)1(0…0)

这里给出一个函数:

int lowbit(int x)
{
    return x&(-x);
}
  (还记得刚刚那个r吗(ridx的二进制表示中最右边的1后面0的个数))两者其实是一样的  即 lowbit(i) ==2^r

读取累积频率

给定索引idx,如果我们想获取累积频率即c[idx],我们只需初始化sum=0, 然后当idx>0时,重复以下操作:sum加上tree[idx], 然后将idx最后的1去掉。 (C++代码如下)

int read(int idx){
	int sum=0;
	while(idx>0){
		sum+=tree[idx];
		idx -= lowbit(idx);
	}
	return sum;
}

为什么可以这么做呢?(重点)

关键是tree数组设计得好。我们知道,tree数组是这么定义的: tree[idx] = f[idx-2r +1] +…+ f[idx]

上面的程序sum加上tree[idx]后,去掉idx最后的1(比如1100变成1000),假设变为idx1,

那么有idx1 = idx-2r (这里的r表示0的个数,上面说过,比如12(1100)去掉12最后的1(即减掉100(4))变成8(1000)), 

sum接下来加上tree[idx1] = f[idx1-2r1 +1] +…+ f[idx1] = f[idx1-2r1 +1] +…+f[idx-2r ], 我们可以看到tree[idx1]表达示的最右元素为f[idx-2r ]

这与tree[idx]表达式的最左元 素f[idx-2r +1]无缝地连接了起来。所以,只需要这样操作下去,即可求得f[1]+…+ f[idx],即c[idx]的结果。

来看一个具体的例子,当idx=13时,初始sum=0:

tree[1101]=f[13]
tree[1100]=f[9]+...+f[12]   (利用上面的解法发现f[12]与f[13]连接在一起)
tree[1000]=f[1]+...+f[8]	(利用上面的解法发现f[8]与f[9]连接在一起)
c[1101]=f[1]+...+f[13]=tree[1101]+tree[1100]+tree[1000]     

read函数迭代的次数是idx二进制表示中位的个数,其最大值为log(MaxVal)。 在本文中MaxVal=16。

时间复杂度:O(log MaxVal)
代码长度:不到10行

当然,你可能会觉得,是怎么想到这个的,感觉现在理解了,但是后面时间久又忘了,我只能说这就是 别人研究那么久的算法的奥妙

废话不多说,现在理解到这已经能懂基本框架,如果还不理解,在看一遍,先不要往下看,后面是基于前面的

上面理解之后, 就能做 最最基础的查找区间和的sum了~~



#include<iostream>
using namespace std;
#define MaxVal 100
int tree[MaxVal],f[MaxVal],c[MaxVal];
int n;
/*
 BIT: 树状数组
MaxVal: 具有非0频率值的数组最大索引,其实就是问题规模或数组大小n
f[i]: 索引为i的频率值,即原始数组中第i个值。i=1…MaxVal
c[i]: 索引为i的累积频率值,c[i]=f[1]+f[2]+…+f[i]
tree[i]: 索引为i的BIT值(下文会介绍它的定义)
num^- : 整数num的补,
 */
//一个二进制函数
int lowbit(int x)
{
    return x&(-x);
}

//得到tree[]
void getTree(){
	//tree[idx] = f[idx-2^r +1] +…+ f[idx].
	//r为idx的二进制表示中最右边的1后面0的个数
	for(int i=1;i<=n;i++){
		tree[i]=0;//初始化
		for(int j=i-lowbit(i)+1;j<=i;j++){
			tree[i]+=f[j];

		}
	}
}
//得到sum(1~idx)
int read(int idx){
	int sum=0;
	while(idx>0){
		sum+=tree[idx];
		idx -=lowbit(idx);
	}
	return sum;
}
int main(){
	int i;
	cin>>n;  //输入16
	//输入f[i] :  1 0 2 1 1 3 0 4 2 5 2 2 3 1 0 2
	for(int i=1;i<=n;i++){
		cin>>f[i];
	}
	getTree();
	for(i=1;i<=n;i++)
			cout<<"f["<<i<<"]="<<f[i]<<endl;
	for( i=1;i<=n;i++)
		cout<<"tree["<<i<<"]="<<tree[i]<<endl;
	for(i=1;i<=n;i++)
			cout<<"c["<<i<<"]="<<read(i)<<endl;
	return 0;


}
/*
 input:
 16
1 0 2 1 1 3 0 4 2 5 2 2 3 1 0 2
output:
f[1]=1
f[2]=0
f[3]=2
f[4]=1
f[5]=1
f[6]=3
f[7]=0
f[8]=4
f[9]=2
f[10]=5
f[11]=2
f[12]=2
f[13]=3
f[14]=1
f[15]=0
f[16]=2
tree[1]=1
tree[2]=1
tree[3]=2
tree[4]=4
tree[5]=1
tree[6]=4
tree[7]=0
tree[8]=12
tree[9]=2
tree[10]=7
tree[11]=2
tree[12]=11
tree[13]=3
tree[14]=4
tree[15]=0
tree[16]=29
c[1]=1
c[2]=1
c[3]=3
c[4]=4
c[5]=5
c[6]=8
c[7]=8
c[8]=12
c[9]=14
c[10]=19
c[11]=21
c[12]=23
c[13]=26
c[14]=27
c[15]=27
c[16]=29
 */


改变某个位置的频率并且更新数组

当我们改变f数组中的某个值,比如f[idx],那么tree数组中哪些元素需要改变呢? 在取累积频率一节,我们每累加一次tree[idx],就将idx最后一个1移除, 然后重复该操作。而如果我们改变了f数组,比如f[idx]增加val,我们则需要为当前索引的 tree数组增加val:tree[idx] += val。然后idx更新为idx加上其最后的一个1, 当idx不大于MaxVal时,不断重复上面的两个操作。详情见以下C++函数:

void update(int idx,int val){
	while(idx<=n){
		tree[idx]+=val;
		idx+=lowbit(idx);
	}
}

总的来说如下图所示每次更新都是更新涵盖当前数组的 数组:比如让f[idx]改变;更新tree的话就像图片那样往左右还有长矩形的更新;

接下来让我们来看一个例子,当idx=5时:tree[5]左边的长矩形有tree[6]、tree[8]、tree[16];

使用上面的算法或者按照图1.6的箭头所示去操作,我们即可更新BIT。

时间复杂度:O(log MaxVal)
代码长度:不到10行

读取某个位置的实际频率

(下面这个是大牛写的;不过我感觉没必要这么麻烦,也不用多开数组啊,之前更新f[idx]的时候直接更新了,访问的时候直接访问f[idx]了不是;

不过大牛既然会想那么麻烦,可能是我哪里想错了,望各位赐教)

(后面想到一个合理的解释,就是我们的更改值是不对f[]造成影响的,只对tree[]和后面要访问的c[]造成影响;这样保证了树状数组初始值f[]始终不变

如果要将之前的操作重置,即撤销之前所有update操作,我们f[]不变,然后在调用getTree()函数,一切就都初始化了,这样可以省空间,

而不必在一开始开一个 数组来copy f[])

(为什么说还要开一个 数组来copy f[]?因为考虑到如果将之前的操作重置(当然ACM做题一般不会出现这种情况都是对树状数组直接输入新的长度,新的f[],所以也不会浪费空间一说)

(当然,有时候考虑时间有时候考虑空间,如果考虑空间的话就用下面这个大牛说的这样,如果考虑时间的话,直接对f[]进行操作,

如果是做ACM的,还是已考虑时间为主,即直接进行操作,因为多开的这个空间算很小的了,不然的话时间多了还多写了一个readSingle函数
)

总的来说:如果是做题,就不要用下面的方法了

下面讲的是针对空间优化的,优化时间的就直接访问f[] 这里不多说了


(在不涉及缩放整个树状数组的时候,上面的话说的是对的(这也是我在学了还有缩放整个树状数组这种东西的时候才知道大牛一开始的目的,),

如果涉及缩放整个树状数组的话就不能这么做了,因为缩放直接对tree[]操作的话会比较快,而如果按照上面讲的,我们肯定是对f[]进行操作,因为对tree[]操作并不会改变f[]的值,如果我们对tree[]操作 访问某点的实际f值却直接访问的话就出错,

当然你可能会问为什么直接对tree[]操作的话会比较快

因为这个是o(n)的时间具体代码看下面的缩放专题,而对f[]操作的话(o(n)),后面还要再进行getTree()操作(o(nlogn))这样是不是用时更多了)


可能这边讲的有点乱,你不知道要采用什么方法,这里总结一下:

如果不涉及 缩放,更改单点的值就直接对f[]操作再update 取f[]就直接取;

如果涉及缩放,就不能对f[]操作了,直接update tree[]  取f[]就利用下面大牛介绍的这种方法取 (如果还有什么疑问或建议,欢迎评论)

下面这段话是大牛写的:

上面我们已经讨论了如何读取指定索引的累积频率值(即c[idx]),很明显我们无法通过 tree[idx]直接读取某个位置的实际频率f[idx]。有人说,我们另外再开一个数组来存储f数 组不就可以了。这样一来,读和存f[idx]都只需要O(1)的时间,而空间复杂度则是O(n)的。 不过如果考虑到节约内存空间是更重要的话,我们就不能这么做了。接下来我们将展示在不 增加内存空间的情况下,如何读取f[idx]。(事实上,本文所讨论的问题都是基于我们只维 护一个tree数组的前提)

事实上,有了前面的讨论,要得到f[idx]是一件非常容易的事: f[idx] = read[idx] - read[idx-1]。即前idx个数的和减去前idx-1个数的和, 然后就是f[idx]了。这种方法的时间复杂度是2*O(log n)。下面我们将重新写一个函数, 来得到一个稍快一点的版本,但其本质思想其实和read[idx]-read[idx-1]是一样的。

假如我们要求f[12],很明显它等于c[12]-c[11]。根据上文讨论的规律,有如下的等式: (为了方便理解,数字写成二进制的表示)

c[14]=c[1110]=tree[1110]+tree[1100]+tree[1000]  

c[13]=c[1101]=tree[1101]+tree[1100]+tree[1000]

f[14]=c[14]-c[13]=tree[1110]-tree[1101]

c[12]=c[1100]=tree[1100]+tree[1000]    
c[11]=c[1011]=tree[1011]+tree[1010]+tree[1000]
f[12]=c[12]-c[11]=tree[1100]-tree[1011]-tree[1010]

从上面3个式子,你发现了什么?没有错,c[12]和c[11](c[14]和c[13])中包含公共部分,而这个公共部分 在实际计算中是可以不计算进来的。那么,以上现象是否具有一般规律性呢?或者说, 我怎么知道,c[idx]和c[idx-1]的公共部分是什么,我应该各自取它们的哪些tree元素来做 差呢?下面将进入一般性的讨论。

让我们来考察相邻的两个索引值idx和idx-1。我们记idx-1的二进制表示为a0b(b全为1), 那么idxa0b+1=a1b^- .(b^- 全为0)。使用上文中读取累积频率的算法(即read函数) 来计算c[idx],当sum加上tree[idx]后(sum初始为0),idx减去最后的1得a0b^- , 我们将它记为z。(如c[14]=c[1110]减去最后的1得z=1100)

用同样的方法去计算c[idx-1],因为idx-1的二进制表示是a0b(b全为1),那么经过一定数量 的循环后,其值一定会变为a0b^- ,(不断减去最后的1),而这个值正是上面标记的z。那么, 到这里已经很明显了,z往后的tree值是c[idx]和c[idx-1]都共有的, 相减只是将它们相互抵消,所以没有必要往下再计算了。

也就是说,c[idx]-c[idx-1]等价于取出tree[idx],然后当idx-1不等于z时,不断地减去 其对应的tree值,然后更新这个索引(减去最后的1)。当其等于z时停止循环(从上面的分析 可知,经过一定的循环后,其值必然会等于z)。下面是C++函数:

int readSingle(int idx){
	int sum =tree(idx);
	
	if(idx>1){   
		int z=idx-lowbit(idx);
		idx--;  //idx=idx-1;
		while(idx!=z){
			sum -=tree[idx];
			idx -=lowbit(idx);
		}
		return sum;
	}
	else if(idx==1) return tree[1];//这里return tree而不是c的原因是 c可能还未更新
	else return -1;
	
}
c[14]=c[1110]减去最后的1得z=1100; sum=tree[14]

计算c[13] 1. sum-=tree[13]  2. idx(1101)-= 1 =1100 ==z break;

f[12]又是怎么算的呢,下面我们来看看根据这个算法,f[12]是怎么计算出来的:

首先,计算z值:z = 12 - (12 & -12) = 8,sum = tree[12] = 11(见表1.1)

对比该算法及调用两次read函数的方法,当idx为奇数时,该算法的时间复杂度仅为O(1), 迭代次数为0(随便拿一组试试)

而对于几乎所有的偶数idx,其时间复杂度为c*O(log idx), 其中c严格小于1。

而read(idx)-read(idx-1)的时间复杂度为c1*O(log idx), 其中c1总是大于1.(故read方法不可取)

时间复杂度:c*O(log MaxVal),c严格小于1
代码长度:不到15行

缩放整个数状数组

有时候我们需要缩放整个f数组,然后更新tree数组。利用上面讨论的结论,我们可以轻松 地达到这个目的。比如,我们要将f[idx]变为f[idx]/c,我们只需要调用上面的update 函数,然后把除以c转变为加上-(c-1)*readSingle(idx)/c即可。这个很容易理解, f[idx]-(c-1)*f[idx]/c = f[idx]/c。用一个for循环即可将所有的tree元素更新。 代码如下:

void scale (int c){
	for(int i=1;i<=n;i++){
		update(-(c-1)*readSingle(i)/c,i);
	}
}

上面的方法似乎有点绕,其实,我们有更快的方法。除法是线性操作,而tree数组中的元素 又是f数组元素的线性组合。因此,如果我们用一个因子去缩放f数组,我们就可以用该因子去 直接缩放tree数组,而不必像上面程序那样麻烦。上面程序的时间复杂度为 O(MaxVal*log MaxVal),而下面的程序只需要O(MaxVal)的时间:

void scale (int c){
	for(int i=1;i<=n;i++){
		tree[i]=tree[i]/c;
	}
}

时间复杂度:O(MaxVal)
代码长度:几行

返回指定累积频率的索引

问题可描述为:给你一个累积频率值cumFre,如果存在c[idx]=cumFre,则返回idx; 否则返回-1。该问题最朴素及最简单的解决方法是求出依次求出c[1]到c[MaxVal], 然后与给出的cumFre对比,如果存在c[idx]=cumFre,则返回idx;否则返回-1。如果f数组中存在负数,那么该方法就是唯一的解决方案。但如果f数组是非负的, 那么c数组一定是非降的。即如果i>=j,则c[i]>=c[j]。这种情况下,利用二分查找的思想, 我们可以写出时间复杂度为O(log n)的算法。

我们从树状数组的最大的2的指数(getTowMax()函数实现)开始找;利用find(int 我们要找的值,int 数组最大的二的指数)

然后这边有个特判,因为我们要从中间找的;所以如果maxtow==n;先判断关键;是否>=然后相应的进行return ;否则 让maxtow除2;

举个例子;如果n==16;先进行特判;如果cumFre(我们要找的那个sum)>tree[16] 返回-1,==返回  16; 否则让maxtow(16) 除2;

从8开始找,这样利用二分最大情况也只是找到15,如果还没有满足条件的就return -1 不会在找到tree[16]了 因为之前不满足;

而如果n==15, 那么maxtow=8;进行二分妥妥的;

至于怎么二分?定义一个temp=maxtow>>=1; 如果cumFre>tree[8] 那么maxtow+=4;这正好满足二分,注意这里,由于tree[]的性质;我们要找的sum

是由一系列的tree[ ]组成的,也就是说我们找的时候是对tree[]操作,那么如果cumFre>tree[8],让cumFre-=tree[8];那么找cumFre就是找更上面的tree[]了 用上面的图理解一下; 当然如果cumFre<tree[8] 不用对cumFre操作,让maxtow-=temp即可;跟着图走一遍应该就可以了

C++函数如下:(如果c数组中存在多个cumFre,find函数返回任意其中一个)

int getTowMax(){
	int sum=1,lastsum=1;
	while(1){
		lastsum=sum<<1;
		if(sum==n)return sum;
		else if(lastsum==n)return lastsum;
		else if(sum<n&&lastsum>n)return sum;
		else sum=lastsum;
	}
}
//二分查找 返回任意一个满足条件的idx
int find(int cumFre,int maxtow){
	int yesORno=-1;int temp=maxtow>>1;
	if(maxtow==n&&cumFre>tree[maxtow])return yesORno;  //特判
	if(maxtow==n&&cumFre==tree[maxtow])return maxtow;
	if(maxtow==n)maxtow>>=1;
	while(1){
	if(temp==0&&cumFre!=tree[maxtow]){break;}

	else if(cumFre>tree[maxtow]){
		cumFre-=tree[maxtow];

		maxtow+=temp;
	}

	else if(cumFre==tree[maxtow]){
		yesORno=maxtow;break;
	}

	else if(cumFre<tree[maxtow]){
		maxtow-=temp;
	}
	temp>>=1;
	}
	return yesORno;
}



时间复杂度:O(log MaxVal)
代码长度:不到20行

 1D小结:只对tree[]进行操作,f[],c[]是虚拟的



#include<iostream>
using namespace std;
#define MaxVal 100
int tree[MaxVal],f[MaxVal],c[MaxVal];
int n;
/*
 BIT: 树状数组
MaxVal: 具有非0频率值的数组最大索引,其实就是问题规模或数组大小n
f[i]: 索引为i的频率值,即原始数组中第i个值。i=1…MaxVal
c[i]: 索引为i的累积频率值,c[i]=f[1]+f[2]+…+f[i]
tree[i]: 索引为i的BIT值(下文会介绍它的定义)
num^- : 整数num的补,
 */
//一个二进制函数
void init(){
	f[0]=c[0]=tree[0]=0;
}
int lowbit(int x)
{
    return x&(-x);
}

//得到tree[]
void getTree(){
	//tree[idx] = f[idx-2^r +1] +…+ f[idx].
	//r为idx的二进制表示中最右边的1后面0的个数
	for(int i=1;i<=n;i++){
		tree[i]=0;//初始化
		for(int j=i-lowbit(i)+1;j<=i;j++){
			tree[i]+=f[j];

		}
	}
}
//得到sum(1~idx)
int read(int idx){
	int sum=0;
	while(idx>0){
		sum+=tree[idx];
		idx -=lowbit(idx);
	}
	return sum;
}
void update(int idx,int val){
	while(idx<=n){
		tree[idx]+=val;
		idx+=lowbit(idx);
	}
}
int readSingle(int idx){
	int sum =tree[idx];

	if(idx>0){
		int z=idx-lowbit(idx);
		idx--;//idx=idx-1;
		while(idx!=z){
			sum -=tree[idx];
			idx -=lowbit(idx);
		}

	}
	//else if(idx==1) return tree[1];//这里return tree而不是c的原因是 c可能还未更新
	//else return -1;
	return sum;
}
//void scale (int c){
//	for(int i=1;i<=n;i++){
//		update(-(c-1)*readSingle(i)/c,i);
//	}
//}
void scale (int c){
	for(int i=1;i<=n;i++){
		tree[i]=tree[i]/c;
	}
}
int getTowMax(){
	int sum=1,lastsum=1;
	while(1){
		lastsum=sum<<1;
		if(sum==n)return sum;
		else if(lastsum==n)return lastsum;
		else if(sum<n&&lastsum>n)return sum;
		else sum=lastsum;
	}
}
//二分查找 返回任意一个满足条件的idx
int find(int cumFre,int maxtow){
	int yesORno=-1;int temp=maxtow>>1;
	if(maxtow==n&&cumFre>tree[maxtow])return yesORno;  //特判
	if(maxtow==n&&cumFre==tree[maxtow])return maxtow;
	if(maxtow==n)maxtow>>=1;
	while(1){
	if(temp==0&&cumFre!=tree[maxtow]){break;}

	else if(cumFre>tree[maxtow]){
		cumFre-=tree[maxtow];

		maxtow+=temp;
	}

	else if(cumFre==tree[maxtow]){
		yesORno=maxtow;break;
	}

	else if(cumFre<tree[maxtow]){
		maxtow-=temp;
	}
	temp>>=1;
	}
	return yesORno;
}

int main(){
	init();
	int i;
	cin>>n;  //输入16
	//输入f[i] :  1 0 2 1 1 3 0 4 2 5 2 2 3 1 0 2
	for(int i=1;i<=n;i++){
		cin>>f[i];
	}
	getTree();
	for(i=1;i<=n;i++)
			cout<<"f["<<i<<"]="<<readSingle(i)<<endl; //不用f[]
	for( i=1;i<=n;i++)
		cout<<"tree["<<i<<"]="<<tree[i]<<endl;
	for(i=1;i<=n;i++)
			cout<<"c["<<i<<"]="<<read(i)<<endl;  //c[]其实是虚拟的不对他进行操作
	int maxtow;
	//得到树状数组2的指数最大的那个  如 n=16 maxtow=16 n=15 maxtow=8
	maxtow=getTowMax();
	//输入一个指定累积频率的索引 即sum 看有没有存在
	int sum;
	while(cin>>sum){
		if(sum==-1)break;
		int mytemp=find(sum,maxtow);
		if(mytemp==-1)cout<<"不存在"<<sum<<endl;
		else cout<<"c["<<mytemp<<"]="<<sum<<"   read()="<<read(mytemp);
	}
	int idx;
	//得到f[5]的实际值
	cin>>idx;//cin>>5
	cout<<readSingle(idx)<<endl;//不要直接取f[]的原因是防止缩放产出错误
	//把5的值为7;
	int val;
	cin>>idx>>val;  //cin>>5>>7
	update(idx,val-readSingle(idx)); //这里不直接val-f[idx]的原因是防止缩放产出错误
	//得到f[5]的实际值
		cin>>idx;//cin>>5
		cout<<readSingle(idx)<<endl;//不要直接取f[]的原因是防止缩放产出错误
		for(i=1;i<=n;i++)
					cout<<"f["<<i<<"]="<<readSingle(i)<<endl;
		for( i=1;i<=n;i++)
				cout<<"tree["<<i<<"]="<<tree[i]<<endl;
	//将所有数都除于2//  如果是要*2 就 /0.5  注意小数的转换  或者把函数重写一下
		int c;
		cin>>c; //cin>>2
		scale(2);
		for(i=1;i<=n;i++)
					cout<<"f["<<i<<"]="<<readSingle(i)<<endl;



	return 0;
}
/*
 input:
 16
1 0 2 1 1 3 0 4 2 5 2 2 3 1 0 2
output:
f[1]=1
f[2]=0
f[3]=2
f[4]=1
f[5]=1
f[6]=3
f[7]=0
f[8]=4
f[9]=2
f[10]=5
f[11]=2
f[12]=2
f[13]=3
f[14]=1
f[15]=0
f[16]=2
tree[1]=1
tree[2]=1
tree[3]=2
tree[4]=4
tree[5]=1
tree[6]=4
tree[7]=0
tree[8]=12
tree[9]=2
tree[10]=7
tree[11]=2
tree[12]=11
tree[13]=3
tree[14]=4
tree[15]=0
tree[16]=29
c[1]=1
c[2]=1
c[3]=3
c[4]=4
c[5]=5
c[6]=8
c[7]=8
c[8]=12
c[9]=14
c[10]=19
c[11]=21
c[12]=23
c[13]=26
c[14]=27
c[15]=27
c[16]=29
 */



2D BIT(Binary Indexed Trees)

BIT可被扩展到多维的情况。假设在一个布满点的平面上(坐标是非负的)。 你有以下三种查询:

  1. 将点(x, y)置1
  2. 将点(x, y)置0
  3. 计算左下角为(0, 0)右上角为(x, y)的矩形内有多少个点(即有多少个1)

如果m是查询次数,max_x和max_y分别是最大的x坐标和最大的y坐标,那么解决该问题的 时间复杂度为O(m*log(max_x)*log(max_y))。在这个例子中,tree是个二维数组。 对于tree[x][y],当固定x坐标时,更新y坐标的过程与一维情况相同。 如果我们想在点(a, b)处置1/0,我们可以调用函数update(a,b,1)/update(a,b,-1), 其中update函数如下:

其中updatey函数与update函数是相似的:

以上两个函数可以整合成一个函数:

其它函数的修改也非常相似,这里就不一一写出来了。此外,BIT也可被扩展到n维的情况。

问题样例

  • SRM 310-FloatingMedian

  • 问题2:

    描述:

    n张卡片摆成一排,分别为第1张到第n张,开始时它们都是下面朝下的。你有两种操作:

    1. T(i,j):将第i张到第j张卡片进行翻转,包含i和j这两张。(正面变反面,反面变正面)
    2. Q(i):如果第i张卡片正面朝下,返回0;否则返回1.

    解决方案:

    操作1和操作2都有O(log n)的解决方案。设数组f初始全为0,当做一次T(i, j)操作后, 将f[i]加1,f[j+1]减1.这样一来,当我们做一次Q(i)时,只需要求f数组的前i项和c[i] ,然后对2取模即可。结合图2.0,当我们做完一次T(i, j)后,f[i]=1,f[j+1]=-1。 这样一来,当k<i时,c[k]%2=0,表明正面朝下;当i<=k<=j时,c[k]%2=1,表明正面朝 上(因为这区间的卡片都被翻转了!);当k>j时,c[k]%2=0,表示卡片正面朝下。 Q(i)返回的正是我们要的判断。

    注意:这里我们使用BIT结构,所以只维护了一个tree数组,并没有维护f数组。 所以,虽然做一次T(i, j)只需要使f[i]加1,f[j+1]减1,但更新tree数组还是需要 O(log n)的时间;而读取c[k]的时间复杂度也是O(log n)。这里其实只用到了一维BIT 的update函数和read函数。

总结

  • 树状数组十分容易进行编程实现
  • 树状数组的每个操作花费常数时间或是(log n)的时间
  • 数状数组需要线性的存储空间(O(n),只维护tree数组)
  • 树状数组可扩展成n维的情况

参考资料

[1] RMQ

[2] Binary Search

[3] Peter M. Fenwick

[4]  hawstein







  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值