一、引言
1.什么是树状数组?
顾名思义,就是用数组来模拟树形结构呗。那么衍生出一个问题,为什么不直接建树?答案是没必要,因为树状数组能处理的问题就没必要建树。和Trie树的构造方式有类似之处。
2.树状数组可以解决什么问题
可以解决大部分基于区间上的更新以及求和问题。
3.树状数组和线段树的区别在哪里
树状数组可以解决的问题都可以用线段树解决,这两者的区别在哪里呢?树状数组的系数要少很多,就比如字符串模拟大数可以解决大数问题,也可以解决1+1的问题,但没人会在1+1的问题上用大数模拟。
4.树状数组的优点和缺点
修改和查询的复杂度都是O(logN),而且相比线段树系数要少很多,比传统数组要快,而且容易写。
缺点是遇到复杂的区间问题还是不能解决,功能还是有限。
二、基本思想
算数基本定理:可以将任意正整数关于2的不重复次幂的唯一分解性质,一个数21可以分解为,2^0+2^2+2^4,则一个区间[1,n]可以二进制分解为log(x)个区间
例如:log(21)=3;
①长度为2^4 [1,2^4]
②长度为2^2 [2^4+1,2^4+2^2]
③[长度为2^0 [2^4+2^2+1,2^4+2^2+2^0]
树状数组就是依赖算术基本定理的分解思想,把一个区间分解为log(n)个小区间,分而治之
三、基本算法
若区间结尾为R,则区间长度就等于R的二进制分解下最小的二次幂,这时引入lowbit(n)
对于序列A,我们建立一个数组c,数组c保存序列A中[x-lowbit[x]+1,x]中所有数的和
该结构满足一下性质:
-
- 每个节点c[x]保存以它为根的子树中所有叶子节点的和
- 每个内部节点c[x]的子节点个数等于lowbit(x)的大小
- 除了树根之外,每一个结点c[x]的父亲是c[x+lowbit[x]]
- 树的深度为log(N)
1.求lowbit(x)
原理:将x用二进制表示,将x取反的基础上再加上1,所以原来最后一位到倒着数原来为1的位置(不包含),这些位置原来从0变成1,又因为加上了一,所以就会进位
原来最后一位到倒着数原来为1的位置(不包含)就会又变成0,直到原来为1的位置与原来是一样的,所以这样就将非负整数x二进制表示下最低位1与后面的0构成的值用lowbit(x)表示
用因为在补码的表示下~x=-1-n
所以lowbit(x)=x&(-x);
2.对某个元素进行加减法操作
对a[x]对于修改,c[x]以及c[x]的祖先都需要修改,又因为c[x]的祖先为c[x+lowbit(x)],所以可以在log(n)的时间内执行单点增加以及前缀和维护操作
3.查询前缀和
对于一个前缀和[1,n],我们把它划分为了log(n)个小区间,想基本思想中举的例子一样,对于区间和,则等于这几个小区间的区间和总值
4.查询区间和
利用前缀和求解,sum[i,j]=sum[j]-sum[i-1]
5.扩展(多维树状数组)
跟一维的差不多,知识多了几个循环,时间复杂度为(logn)^m,在维度不大的情况下,还是可以接受的
6.注意事项
下标不能为0,lowbit(0) ,会陷入死循环
四、典例分析
模板1(单点修改)
#includeusing namespace std; int n,m; int a[50005],c[50005]; //对应原数组和树状数组 int lowbit(int x){ return x&(-x); } void updata(int i,int k){ //在i位置加上k while(i <= n){ c[i] += k; i += lowbit(i); } } int getsum(int i){ //求A[1 - i]的和 int res = 0; while(i > 0){ res += c[i]; i -= lowbit(i); } return res; } int main(){ int t; cin>>t; for(int tot = 1; tot <= t; tot++){ cout << "Case " << tot << ":" << endl; memset(a, 0, sizeof a); memset(c, 0, sizeof c); cin>>n; for(int i = 1; i <= n; i++){ cin>>a[i]; updata(i,a[i]); //输入初值的时候,也相当于更新了值 } string s; int x,y; while(cin>>s && s[0] != 'E'){ cin>>x>>y; if(s[0] == 'Q'){ //求和操作 int sum = getsum(y) - getsum(x-1); //x-y区间和也就等于1-y区间和减去1-(x-1)区间和 cout << sum << endl; } else if(s[0] == 'A'){ updata(x,y); } else if(s[0] == 'S'){ updata(x,-y); //减去操作,即为加上相反数 } } } return 0; }
模板2(区间修改)
int n,m; int a[50005] = {0}; int sum1[50005]; //(D[1] + D[2] + ... + D[n]) int sum2[50005]; //(1*D[1] + 2*D[2] + ... + n*D[n]) int lowbit(int x){ return x&(-x); } void updata(int i,int k){ int x = i; //因为x不变,所以得先保存i值 while(i <= n){ sum1[i] += k; sum2[i] += k * (x-1); i += lowbit(i); } } int getsum(int i){ //求前缀和 int res = 0, x = i; while(i > 0){ res += x * sum1[i] - sum2[i]; i -= lowbit(i); } return res; } int main(){ cin>>n; for(int i = 1; i <= n; i++){ cin>>a[i]; updata(i,a[i] - a[i-1]); //输入初值的时候,也相当于更新了值 } //[x,y]区间内加上k updata(x,k); //A[x] - A[x-1]增加k updata(y+1,-k); //A[y+1] - A[y]减少k //求[x,y]区间和 int sum = getsum(y) - getsum(x-1); return 0; }
五、相关转载与推荐文章(十分感谢这些博主)
树状数组详解
l