下面是树状数组的学习笔记:ios
在解题过程当中,有时候要维护一个数组的前缀\(S[i]=A[1]+A[2]+···+A[i]\)。可是若是咱们修改了任何一个\(A[i]\),\(S[1]、S[2]、S[3]···S[i]\)的值都会相应改变。若是咱们循环维护每个\(S[i]\),那样就太慢了。因而咱们要考虑使用数据结构来进行优化。树状数组就是这样一个数据结构!数组
首先介绍一下前置知识:数据结构
根据任意正整数关于\(2\)的不重复次幂的惟一分解性质,若一个正整数\(x\)的\(2\)进制表示为\(10101\),其中等于\(1\)的位是\(0,2,4\)则正整数能够被分解为\(2^4+2^2+2^0\)。那么,区间\([1,x]\)就能够被分为\(logx\)各小区间函数
长度为\(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]\)。学习
树状数组就是这样一种结构,它的基本用途就是维护序列的前缀和。对于区间\([1,x]\)树状数组将其分为\(logx\)个子区间,从而知足快速询问区间和。优化
由上文可知,这些子区间的共同特色是:若区间结尾为\(R\)则区间长度就为\(R\)的“二进制分解”下最小的\(2\)的次幂,咱们定义为\(lowbit(R)\)。对于给定的\(A\)序列,咱们维护一个\(c\)数组,其实这个\(c\)数组就是咱们所说的树状数组。\(c[i]\)中储存的是\(\sum A[i]\space (i\in [x-lowbit(x)+1,x])\)。spa
树状数组的存储结构以下图:code
值得注意的是,这样的树状结构知足如下的4个性质:get
\(1\). 每一个内部节点\(c[i]\)保存以它为根的子树中全部叶节点的和it
\(2\). 每一个内部节点\(c[i]\)的子节点个数为\(lowbit(i)\)的大小
\(3\). 除树根外,每一个内部节点\(c[i]\)的父节点是\(c[i+lowbit(i)]\)
\(4\). 树的深度为\(O(logN)\)
接下来就讲到实现方面了,树状数组的实现是与上面\(4\)个性质分不开的。
\(lowbit(n)\)表示去除非负整数\(n\)在二进制下最低位的\(1\)以及它后边的\(0\)构成的数值。因而\(lowbit(n)=n\)&\((-n)\),可是这是怎么来的呢,请听我细细道来(其实我也不知道)
设\(n>0\),\(n\)的第\(k\)位是1,其他都是\(0\)
为了实现\(lowbit\)运算,先把\(n\)取反,此时第\(k\) 位变为\(0\),其他都是\(1\)。再另\(n=n+1\),此时由于进位 ,第\(k\)位变为\(1\),其他都为\(0\)。在上面的取反加一操做后,\(n\)的第\(k+1\)到最高位刚好与原来相反,因此\(n\)&~\(n+1\)仅有第\(k\)位为\(1\),其他位都是\(0\)。而在补码表示下,~\(n=-1-n\),所以,\(lowbit(n)=n\)&\((-n)\)
下面是这一部分的代码实现:
int lowbit(int n) { return n&(-n);//这句话水的不能再水了,不说了 }
树状数组支持单点操做,根据性质\(一、3\)。每个内部节点\(c[i]\)保存以它为根的子树中全部叶节点的和,这就说明,咱们一旦对$A[i] \(进行改动,就必定要对\)c[i]\(进行改动,而且将全部\)c[i]\(的父亲节点改动。那么如何找到\)c[i]\(的父亲节点呢?其实咱们只须要将\)i\(更新为\)i+lowbit(i)$,就能够完成对父亲节点的遍历了。
下面是这一部分的代码实现:
void update(int x,int y)//表示A[x]加y时维护c数组 { for(;x<=n;x+=lowbit(x)) c[x]+=y; }
树状数组支持单点修改和区间查询,下面介绍一下区间查询前缀和的方式:
在这里咱们所定义的前缀和,实际上就是\(\sum A[i] (i\in [1,x])\)。按照(\(1\))中所讲,应该将$[1,x] \(分红\)logn\(个小区间,每一个小区间的区间和都已经保存在数组\)c$中。
下面是这一部分的代码实现:(即\(O(logn)\)时间查询前缀和)
int sum(int x)//求1~x的前缀和 { int ans=0;//用ans存储前缀和 for(;x;x-=lowbit(x)) ans+=c[i];//遍历子节点,累加ans return ans;//最后的答案就是ans }
调用以上的\(sum\)函数,能够求出\(A[x]···A[y]\)得值就是\(sum(y)-sum(x-1)\)
因为处理每个树状数组的复杂度是\(O(logn)\)。因此它能够扩充到\(m\)维,这样一来,处理树状数组的复杂度就提高到了\(O(log^mn)\),若\(m\)不大的时候,这个复杂度是能够被接受的。可是到底如何实现呢?
其实,扩充树状数组的方式就是讲原来的一个循环改为\(m\)个循环,这一部分的代码以下:
void update(int x,int y,int z)//将(x,y)的值加上z { int i=x; while(i<=n)//若是m更大,再多写几个while { int j=y; while(j<=m) { c[i][j]+=z; j+=lowbit(j); } i+=lowbit(i); } }
int sum(int x,int y) { int res=0,i=x; while(i>0) { int j=y; while(j>0) { res+=c[i][j]; j-=lowbit(i); } i-=lowbit(i); } return res; }
要注意树状数组绝对不能出现下标为\(0\)的情况,由于啥啊,由于\(lowbit(0)=0\)啊!
树状数组的应用有不少,下面重点将洛谷上两个模板题讲解一下
【题目描述】
如题,已知一个数列,你须要进行下面两种操做:
\(1\).将某一个数加上\(x\)
\(2\).求出某区间每个数的和
直接上代码吧,就是不折不扣的板子题
#include<cstdio> #include<iostream> using namespace std; const int maxn=500010; int n,m; int a[maxn],c[maxn]; int h,q,k;//我没得设变量啊TwT int lowbit(int x)//就是算lowbit(x) { return x&(-x); } void update(int x,int y)//就是将y插入到x中 { for(;x<=n;x+=lowbit(x)) { c[x]+=y; } } int sum(int x)//就是计算A[1]+A[2]+···+A[x]的值 { int ans=0; for(;x;x-=lowbit(x)) { ans+=c[x]; } return ans; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) { scanf("%d",&a[i]); update(i,a[i]); } while(m)//循环读入数据 { m--; scanf("%d",&h); if(h==1)//h是对类型的判断 { scanf("%d%d",&q,&k); update(q,k); } if(h==2) { scanf("%d%d",&q,&k); printf("%d\n",sum(k)-sum(q-1)); } } return 0; }
上面的这道题是典型的树状数组“单点修改,区间查询题”。可是下面一道题就不是这样了!
【题目描述】
如题,已知一个数列,你须要进行下面两种操做:
\(1\).将某区间每个数数加上\(x\)
\(2\).求出某一个数的值
【题目分析】
诶?这道题好像是“区间修改,单点查询”的题额,怎么作呢?咱们引入一个叫作差分的概念:
假设\(A[]=\){\(1,5,3,6,4\)},\(B[]=\){\(1,4,-2,3,-2\)}
咱们能够发现,若是规定\(A[0]=0\),那么能够知足\(B[i]=A[i]-A[i-1]\),而且\(A[i]=B[1]+B[2]+···+B[i]\)。
若是咱们在区间\([2,4]\)上加上\(2\),按照相同的规则进行处理,能够获得\(B[]=\){\(1,7,-2,3,\)},只有\(B[2]\)和\(B[5]\)有改动
因而咱们能够获得一个规律,即:对于\(A\),将区间\([l,r]\)中的每个数都加上一个数\(x\),\(B[l]+=x;B[r+1]-=x\)
因而咱们能够开一个树状数组维护差分值,每次只须要将\(l\)和\(r+1\)进行操做,最后将须要查询的\(A\)值转化为\(B\)值求和就能够获得了!
下面请食用代码:
#include<cstdio> #include<iostream> using namespace std; const int maxn=500010; int n,m; int a[maxn],c[maxn]; int h,q,k,d; int lowbit(int x) { return x&(-x); } void update(int x,int y) { for(;x<=n;x+=lowbit(x)) { c[x]+=y; } } int sum(int x) { int ans=0; for(;x;x-=lowbit(x)) { ans+=c[x]; } return ans; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) scanf("%d",&a[i]); while(m) { m--; scanf("%d",&h); if(h==1) { scanf("%d%d%d",&q,&k,&d); update(q,d); update(k+1,-d); } if(h==2) { scanf("%d",&q); printf("%d\n",a[q]+sum(q)); } } return 0; } /* 相信你们已经发现了,这段代码与前一段没有什么太大的区别,其实就是将读入增长了一个,输出减小了一个罢了 */
\(end\)