详解树状数组三种模型

来源:互联网 发布:dj小可网络电音歌曲 编辑:程序博客网 时间:2024/05/20 20:46

首先说明下:最后的最大值模型的代码没有测试,不过应该是没问题的。其它三个更新求和的模型的代码借鉴于网上的各个博文,应该是没问题的。其中前两个模型的代码已测试无误。

树状数组与线段树在思想上很类似的一种数据结构,它比线段树更简洁,但它的适用范围也小了些。

提供一篇博文,详解树状数组的:http://www.hawstein.com/posts/binary-indexed-trees.html

树状数组是一个可以高效的进行区间统计的数据结构,它的本质即为将区间和通过某个特定的函数分为几段存到数组中,树状数组中的元素值对应原序列某一段特定区间的区间和,所以它可以进行区间查询和更新点,所以树状数组常见于区间求和问题。

首先来介绍一下树状数组的原理。首先,每个整数都能以二进制的方式表示,即由一段连续的0和1表示,0代表没有,1代表有。如:13=1101(2)。记lowbit表示为x的二进制表示中的最后一个1。通过函数lowbit(x)=x&(-x )可以求得该数。这样,我们建立一个一维数组tree[],其中tree[i]表示[i-lowbit(i)+1,i]这个区间内所有元素的和。这样,我们就写出了一个树状数组。具体的记录方式如下图所示。


该表也反映了树状数组的记录方式,f[i]为i的前缀和,即[1,i]内的元素和,c[i]为第i位上元素的值,tree[i]为树状数组中第i位记录的值,综合该表格和上图,我们可以仔细理解一下树状数组的记录方式。

查询区间和:

在建立好了树状数组之后,我们要进行应用。首先介绍怎么查询前缀和,即对于i,求[1,i]的区间和。由树状数组定义可得,sum[1,1111(2)]=tree[1111]+tree[1110]+tree[1100]+tree[1000]。显然,这是一个很方便的操作,我们的数组设计的就是这样:tree[1111]=f[1111],tree[1110]=f[1101]+...+f[1110],tree[1100]=f[1001]+...+f[1100],tree[1000]=f[0001]+...+f[1000]。f[i]为原序列中下标为i对应的值。所以求前缀和的代码是

int query(int i){int sum = 0;while (i){sum += tree[i];i -= i&(-i);}return sum;}


所以求[l,r]上的区间和sum=query(r)-query(l-1);操作的次数是二进制中1的个数,所以复杂度是O(log n)。

接下来介绍更新点,由于树状数组中保存的是某一段特定区间的区间和,所以在更新点时,所有包括了这个点的区间和都要更新。由于我们是向上存储的。所以要更新该点以及后面中包括了该点的区间和。代码如下:

void update(int i, int val){while (i <= n){tree[i] += val;i += i&(-i);}}


至此介绍完了树状数组的查询区间和更新点,这是树状数组最基本的用途。

接下来我们来介绍树状数组的其他几个模型。

 

第二种模型——树状数组的更新区间和查询点模型。

现在我们假设原序列是序列A,我们要维护的是A序列,要对A进行更新区间和查询点。然后我们建立一个初值全为0的B序列。B(i)表示我们对[1,i]整体总共被加了多少。B(i)=c表示我们把A(1)到A(i)的值全部都加上了c。如我们将[1,i]上的值都加上a,则B(i)+=a,如果我们又要对[1,i]上的值都加上-b,则B(i)-=b。设函数ADD(x,c)表示将[1,x]上的值都加上c,则显然当且仅当x>=i时,该操作会对A(i)造成影响。又如果我们的A(i)的初始值为0,则A(i)=B(i)+...+B(N)!而对于区间更新,将B(i)加上c即可。

现在我们假设序列A的初值全为0,所以现在如果我们要将A的[l,r]上的值加上c,只需B(r)+=c,B(l-1)-=c。如果我们要查询A(i)的值,A(i)=B(i)+...+B(N)。

现在我们要维护A序列,只需维护B序列即可,对A序列更新区间即相当于对B序列更新两个点,对A序列查询点即相当于对B序列查询区间。所以我们对B序列的操作就是查询区间和更新点,所以我们可以将B序列建成一棵树状数组,这样我们通过树状数组也实现了更新区间和查询点,只不过这个树状数组的元素不是我们所求序列的元素值,而是它们之间有一个类似于函数的关系。

但是,由于我们原本的树状数组是查询的前缀和,而在本模型中我们查询的是后缀和B(i)+...+B(N),所以,我们之前的树状数组是向下查询向上更新,本模型中就是向上查询向下更新,其实就是树状数组中每一个元素存储的区间变了,之前是从该点向下扩展的一个区间,而现在是从该点向上扩展的一个区间。

更新区间代码

void update(int l, int r, int val){for (int i = r; i>0; i -= i&(-i))B[i] += c;for (int i = l - 1; i>0; i -= i&(-i))B[i] += (-c);}


查询点的代码

int query(int i){int sum = 0;while (i <= n){sum += B[i];i += i&(-i);}return sum;}


第三种模型——树状数组的更新区间和查询区间模型
这个模型就相对复杂一些,除了原来的序列A之外,还需要B和C两个辅助序列。
设A序列为初始的序列,B序列同第二种模型,B(i)表示对区间[1,i]整体总共被加了多少。C(i)表示[1,i]整体每个数被加的数的总和。即对于ADD(x,c)(把区间[1,x]上的元素都加上c),B(x)+=c,C(x)+=c*x。
而ADD(x,c)是这样影响[1,i]的区间和的:若x<i,则会将A[1,i]上的区间和加上x*c,否则(即x>=i),会将A[1,i]上的区间和加上i*c。所以[1,i]的区间和=(B(i),...,B(N))*i加上C(1),...,C(i-1)。
那为什么我们这样求A[1,i]的区间和呢?,我们已知[1,i]的元素整体都被加上了B(i),...,B(N)这些数,然后还有我们的一些更新的操作只影响到了[1,i]里的部分元素。如我们想求区间[1,10]的区间和,那如果我们之前对区间[1,3][1,7]等[1,10]的子集进行了区间更新,那么这些操作就只会影响到区间里的部分元素,所以我们也不能用B(3)*10,B(7)*10,我们应该写成B(3)*3,B(7)*7,但这样就意味着我们 需要记录之前进行了哪些更新的操作,并且我们还要判断哪些更新操作是对所求区间的子区间进行的更新操作,但这样的时间复杂度和空间复杂度就大大增加了,为了使我们的算法保持优秀,所以我们才有了C这个辅助数组,对于那些所求区间的子集里的更新操作,我们都可以用C(1),C(2),...,C(i-1)来记录,比如C(2),表示我们对[1,2]这个区间进行的更新操作,但如果我们对这个区间没有进行更新操作,那C(2)就为0,所以C(1),C(2),...,C(i-1)就可以将[1,i]的子区间里进行的区间更新操作记录完全。当然,我们对于那些包含区间[1,i]的区间更新操作,就不能用C()来求,因为C()是记录的对应整个大区间的区间的所有值的更新的和,我们所求的只是其中的一部分,所以就是用(B(i),...,B(N))*i来记录。所以我们想要求区间和,就分成了这两部分。所以[1,i]的区间和=(B(i),...,B(N))*i加上C(1),...,C(i-1)。
对于区间更新操作,B数组和C数组都要更新。对于区间查询操作,我们也是对B数组和C数组都要查询,代码如下:

void update_B(int x, int c){while (x){B[x] += c;x -= x&(-x);}}void update_C(int x, int c){while (x <= n){C[x] += x*c;x += x&(-x);}}

int sum_B(int x){int s = 0;while (x <= n){s += B[x];x += x&(-x);}return s;}int sum_C(int x){int s = 0;while (x){s += C[x];x -= x&(-x);}return s;}


对于更新区间,使[l,r]上都加上c,则执行update(r,c)和update(l-1,-c)。对于查询区间[l,r]的区间和,则等于sum_B(r)*r+sum_C(r)-sum_B(l-1)*(l-1)-sum_C(l-1)。

至此树状数组常见的三种模型已经介绍完毕。

我们再来介绍树状数组的存储最大值的模型。

其实就是树状数组第一种模型的所存储的东西发生了变化,存储的不再是区间和,而是这个区间内的最大值,由于第一种模型是改点求段型,所以我们这个也是改点求段型,不难发现,对于存储最大值,进行改点求段还是很方便的。

查询区间[1,i]的最大值的代码:

int query(int i){int max = 0x7fffffff;while (i){if (max<tree[i])max = tree[i]i -= i&(-i);}return sum;}


因为树状数组中已经是存储的最大值,其他的值都已经没有,所以,只能查询[1,i]区间的最大值。

更新点的代码:

void update(int i, int val){while (i <= n){if (tree[i]<val)tree[i] = val;i += i&(-i);}}



0 0