本博客是由一个蒟蒻编写,内容可能出错,若发现请告诉本蒟蒻,以便大众阅读
转载请注明原网址:http://www.javashuo.com/article/p-ecquzhwr-nv.htmlhtml
众所周知, 线段树和树状数组是兄弟来的ios
树状数组能够解的,线段树能解
树状数组不能够解的,线段树仍是能够解
既然这样,那我学会线段树不就搞定了吗,干吗还学树状数组呀数组
其实呢,就是码量少,思惟清晰吧
对比一下
单点修改区间查询
线段树100行起步
树状数组呢,50行左右吧
区间修改区间查询
线段树估计要飙到150了吧
树状数组依旧50行
没有对比就没有伤害呀
这时,有些线段树忠实粉或许会思考人生:你看我还有机会吗?
机会是有的,那就是,打树状数组吧(固然有些题仍是要打线段树的啦)函数
此章节内容部分引用自bestsort的小站
众所周知,一棵满二叉树长样:
挪一下位置后,变成了这样
上面这个就是树状数组的画法
准确来讲,这时求和数组的画法
把原数组\(a\)也加进来,成了这样(\(c\)是求和数组)
\(c[i]\)表示子树叶子节点的权值
如上图,有
\(c[1]=a[1]\\ c[2]=a[1]+a[2]\\ c[3]=a[3]\\ c[4]=a[1]+a[2]+a[3]+a[4]\\ c[5]=a[5]\\ c[6]=a[5]+a[6]\\ c[7]=a[7]\\ c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]\)
转换成二进制再来看一眼
\(c[1]=c[0001]=a[1]\\ c[2]=c[0010]=a[1]+a[2]\\ c[3]=c[0011]=a[3]\\ c[4]=c[0100]=a[1]+a[2]+a[3]+a[4]\\ c[5]=c[0101]=a[5]\\ c[6]=c[0110]=a[5]+a[6]\\ c[7]=c[0111]=a[7]\\ c[8]=c[1000]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]\)
对照式子能够发现,对于一个\(i\)
\(c[i]=a[i-2^k+1]+a[i-2^k+2]+a[i-2^k+3]……+a[i]\)(\(k\)为二进制下\(i\)最低位的1后面的0的个数,例如8对应的\(k\)就等于3,由于\(8_{10}=1000_2\),最低位的1后面有3个0)
这时候,问题就来了,\(2^k\)怎么求???学习
\(lowbit\)函数就是用来求\(2^k\)是多少的
具体操做是spa
int lowbit(int x) {return x&(-x);}
解释
“&”这个符号在C++中指的是按位与运算,具体是说,若在二进制下相同的位置两数都为1,那么&出的答案这一位也为1,不然为0
例如\(12\&6\)
\(12_{10}=1100_2\)code
\(6_{10}=0110_2\)(空位用0补齐)htm
\(ans=0100_2=4_{10}\)
在上面这个数据中,12和6只有第三个位置上才都是1,那么答案也就只有这个位置上是1
( 不过学树状数组的人应该都不会不知道位运算吧)
那么\(x\&(-x)\)是什么意思呢
首先说明\(-x\)在二进制下和\(x\)的关系
在二进制下,\(-x\)就是\(x\)取反后再加1
例如,\(10_{10}=01010_2\),那么\(-10_{10}=10101_2+1_2=10110_2\)(第一位是符号位)
进行按位与运算后,答案就是\(00010_2=2^1=2_{10}\)(第一位是符号位)
眼睛扫一扫,发现答案就是\(2\)
神奇吧
具体证实呢,我也不会,嘻嘻(毕竟我只是一个蒟蒻)blog
若要更新当前节点的\(a[i]\)
那么是否是能够直接更新\(a[i]\)的上级,\(a[i]\)上级的上级,以此类推
用\(lowbit\)到上级所在下标图片
void update(int now,int x) { int i; for (i=now;i<=n;i+=lowbit(i)) c[i]+=x; }
对于区间查询,咱们采起前缀和的求法
对于一个区间\([l,r]\),咱们求出\(r\)的前缀和,减去\(l-1\)的前缀和即为答案
查询的具体过程呢,也很简单
就是从要查的节点以此往下,搜索下级
依旧是用\(lowbit\)
int get(int x) { int i,ans; ans=0; for (i=x;i>=1;i-=lowbit(i)) ans+=c[i]; return ans; }
#include<cstdio> #include<iostream> using namespace std; long long n,m,i,x,y,ch,c[1000005]; long long lowbit(long long x) { return x&(-x); } void update(long long now,long long x) { long long i; for (i=now;i<=n;i+=lowbit(i)) c[i]+=x; } long long get(long long x) { long long i,ans; ans=0; for (i=x;i>=1;i-=lowbit(i)) ans+=c[i]; return ans; } int main() { scanf("%lld%lld",&n,&m); for (i=1;i<=n;i++) { scanf("%lld",&x); update(i,x); } for (i=1;i<=m;i++) { scanf("%lld%lld%lld",&ch,&x,&y); if (ch==2) printf("%lld\n",get(y)-get(x-1)); else update(x,y); } return 0; }
引入差分的思想,记录数组里每一个元素与前一个元素的差,那么\(a_i=\sum_{j=1}^i d_j\),若是修改区间\([l,r]\),令其加上\(x\),那么\(l\)与\(l-1\)的差增长了\(x\),\(r\)与\(r+1\)的差减少了\(x\),根据差分,就能够给\(d_{l}\)加上\(x\),给\(d_{r+1}\)减去\(x\)
直接根据\(a_i=\sum_{j=1}^i d_j\),查前缀和就好
#include<cstdio> using namespace std; int n,m,i,l,r,x,bj; long long a[1000005],c[1000005]; int lowbit(int x) { return x&(-x); } void update(int now,int x) { int i; for (i=now;i<=n;i+=lowbit(i)) c[i]+=x; } long long get(int x) { int i; long long ans; ans=0; for (i=x;i;i-=lowbit(i)) ans+=c[i]; return ans; } int main() { scanf("%d%d",&n,&m); for (i=1;i<=n;i++) { scanf("%lld",&a[i]); update(i,a[i]-a[i-1]); } for (i=1;i<=m;i++) { scanf("%d",&bj); if (bj==1) { scanf("%d%d%d",&l,&r,&x); update(l,x); update(r+1,-x); } else { scanf("%d",&x); printf("%lld\n",get(x)); } } return 0; }
这个也是线段树最麻烦的地方,一般100行起步,但树状数组就不用了,实测50行不到,并且我不压行
先看一下若是按照问题2的方法来求区间前缀和,要怎么求
位置\(x\)的前缀和=\(\sum_{i=1}^x\sum_{j=1}^id_j\),发如今这个式子里,\(d_1\)被计算了\(x\)此,\(d_2\)被计算了\(x-1\)次……,\(d_x\)被计算了1次。那么这个式子就能够转化为
\(\sum_{i=1}^xd_i\times(x-i+1)=(x+1)\sum_{i=1}^xd_i-\sum_{i=1}^xd_i\times i\)
其中\(x+1\)是给出的,那么咱们记录\(d_i\)和\(d_i\times i\)就能够了
维护两个数组\(sum1\)和\(sum2\),分别记录\(d_i\)和\(d_i\times i\)
\(sum1\)同问题2的\(d\),\(sum2\)也相似,\(l\)加上\(l\times x\),\(r+1\)减去\((r+1)x\)
单点\(x\)的前缀和就是\((x+1)\times sum1\)中\(x\)的前缀和-\(sum2\)中\(x\)的前缀和,区间\([l,r]\)的值就是\(r\)的前缀和-\(l-1\)的前缀和
#include<cstdio> using namespace std; long long n,m,i,l,r,x,bj,a[1000005],c1[1000005],c2[1000005]; long long lowbit(long long x) { return x&(-x); } void update(long long k,long long x) { long long i; for (i=k;i<=n;i+=lowbit(i)) { c1[i]+=x; c2[i]+=x*k; } } long long get(long long x) { long long i,ans; ans=0; for (i=x;i;i-=lowbit(i)) ans+=((x+1)*c1[i])-c2[i]; return ans; } int main() { scanf("%lld%lld",&n,&m); for (i=1;i<=n;i++) { scanf("%lld",&a[i]); update(i,a[i]-a[i-1]); } for (i=1;i<=m;i++) { scanf("%lld",&bj); if (bj==1) { scanf("%lld%lld%lld",&l,&r,&x); update(l,x); update(r+1,-x); } else { scanf("%lld%lld",&l,&r); printf("%lld\n",get(r)-get(l-1)); } } return 0; }
线段树与树状数组有不少类似的地方,可是树状数组很明显的优点就是短,可是线段树能够处理不少种状况,而这里面有些是树状数组作不到的,因此说不管是线段树仍是树状数组,咱们都应该学习一下,而后选择更好的去解决题目。
不定时更新高阶操做