
本文共 6722 字,大约阅读时间需要 22 分钟。
告知
本博客是由一个蒟蒻编写,内容可能出错,若发现请告诉本蒟蒻,以便大众阅读
转载请注明原网址:https://blog.csdn.net/LZX_lzx/article/details/107598565
树状数组和线段树
众所周知, 线段树和树状数组是兄弟来的
它们之间的关系
树状数组可以解的,线段树能解
树状数组不可以解的,线段树还是可以解
既然这样,那我学会线段树不就搞定了吗,干嘛还学树状数组呀
那么,树状数组优在何处呢?
其实呢,就是码量少,思维清晰吧
对比一下
单点修改区间查询
线段树100行起步
树状数组呢,50行左右吧
区间修改区间查询
线段树估计要飙到150了吧
树状数组依旧50行
没有对比就没有伤害呀
这时,有些线段树忠实粉或许会思考人生:你看我还有机会吗?
机会是有的,那就是,打树状数组吧(当然有些题还是要打线段树的啦)
树状数组简介
树状数组图解
此章节内容部分引用自
众所周知,一棵满二叉树长样:
挪一下位置后,变成了这样
上面这个就是树状数组的画法
准确来说,这时求和数组的画法
把原数组 a a a也加进来,成了这样( c c c是求和数组)
c [ i ] c[i] 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]=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]=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 ] 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] 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 i i
c [ i ] = a [ i − 2 k + 1 ] + a [ i − 2 k + 2 ] + a [ i − 2 k + 3 ] … … + a [ i ] c[i]=a[i-2^k+1]+a[i-2^k+2]+a[i-2^k+3]……+a[i] c[i]=a[i−2k+1]+a[i−2k+2]+a[i−2k+3]……+a[i]( k k k为二进制下 i i i最低位的1后面的0的个数,例如8对应的 k k k就等于3,因为 8 10 = 100 0 2 8_{10}=1000_2 810=10002,最低位的1后面有3个0)
这时候,问题就来了, 2 k 2^k 2k怎么求???
引入 l o w b i t lowbit lowbit
l o w b i t lowbit lowbit函数就是用来求 2 k 2^k 2k是多少的
具体操作是
int lowbit(int x) { return x&(-x);}
解释
“&”这个符号在C++中指的是按位与运算,具体是说,若在二进制下相同的位置两数都为1,那么&出的答案这一位也为1,否则为0
例如 12 & 6 12\&6 12&6
1 2 10 = 110 0 2 12_{10}=1100_2 1210=11002
6 10 = 011 0 2 6_{10}=0110_2 610=01102(空位用0补齐)
a n s = 010 0 2 = 4 10 ans=0100_2=4_{10} ans=01002=410
在上面这个数据中,12和6只有第三个位置上才都是1,那么答案也就只有这个位置上是1
( 不过学树状数组的人应该都不会不知道位运算吧)
那么 x & ( − x ) x\&(-x) x&(−x)是什么意思呢
首先说明 − x -x −x在二进制下和 x x x的关系
在二进制下, − x -x −x就是 x x x取反后再加1
例如, 1 0 10 = 0101 0 2 10_{10}=01010_2 1010=010102,那么 − 1 0 10 = 1010 1 2 + 1 2 = 1011 0 2 -10_{10}=10101_2+1_2=10110_2 −1010=101012+12=101102(第一位是符号位)
进行按位与运算后,答案就是 0001 0 2 = 2 1 = 2 10 00010_2=2^1=2_{10} 000102=21=210(第一位是符号位)
眼睛扫一扫,发现答案就是 2 2 2
神奇吧
具体证明呢,我也不会,嘻嘻(毕竟我只是一个蒟蒻)
基本应用
1.单点修改,区间查询
修改
若要更新当前节点的 a [ i ] a[i] a[i]
那么是不是可以直接更新 a [ i ] a[i] a[i]的上级, a [ i ] a[i] a[i]上级的上级,以此类推
用 l o w b i t lowbit lowbit到上级所在下标
void update(int now,int x){ int i; for (i=now;i<=n;i+=lowbit(i)) c[i]+=x;}
查询
对于区间查询,我们采取前缀和的求法
对于一个区间 [ l , r ] [l,r] [l,r],我们求出 r r r的前缀和,减去 l − 1 l-1 l−1的前缀和即为答案
查询的具体过程呢,也很简单
就是从要查的节点以此往下,搜索下级
依旧是用 l o w b i t lowbit lowbit
int get(int x){ int i,ans; ans=0; for (i=x;i>=1;i-=lowbit(i)) ans+=c[i]; return ans;}
题目
Code
#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;}
2.区间修改,单点查询
修改
引入差分的思想,记录数组里每个元素与前一个元素的差,那么 a i = ∑ j = 1 i d j a_i=\sum_{j=1}^i d_j ai=∑j=1idj,如果修改区间 [ l , r ] [l,r] [l,r],令其加上 x x x,那么 l l l与 l − 1 l-1 l−1的差增加了 x x x, r r r与 r + 1 r+1 r+1的差减小了 x x x,根据差分,就可以给 d l d_{l} dl加上 x x x,给 d r + 1 d_{r+1} dr+1减去 x x x
查询
直接根据 a i = ∑ j = 1 i d j a_i=\sum_{j=1}^i d_j ai=∑j=1idj,查前缀和就好
题目
Code
#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; }
3.区间修改,区间查询
这个也是线段树最麻烦的地方,通常100行起步,但树状数组就不用了,实测50行不到,而且我不压行
先看一下如果按照问题2的方法来求区间前缀和,要怎么求
位置 x x x的前缀和= ∑ i = 1 x ∑ j = 1 i d j \sum_{i=1}^x\sum_{j=1}^id_j ∑i=1x∑j=1idj,发现在这个式子里, d 1 d_1 d1被计算了 x x x此, d 2 d_2 d2被计算了 x − 1 x-1 x−1次……, d x d_x dx被计算了1次。那么这个式子就可以转化为
∑ i = 1 x d i × ( x − i + 1 ) = ( x + 1 ) ∑ i = 1 x d i − ∑ i = 1 x d i × i \sum_{i=1}^xd_i\times(x-i+1)=(x+1)\sum_{i=1}^xd_i-\sum_{i=1}^xd_i\times i ∑i=1xdi×(x−i+1)=(x+1)∑i=1xdi−∑i=1xdi×i
其中 x + 1 x+1 x+1是给出的,那么我们记录 d i d_i di和 d i × i d_i\times i di×i就可以了
维护两个数组 s u m 1 sum1 sum1和 s u m 2 sum2 sum2,分别记录 d i d_i di和 d i × i d_i\times i di×i
修改
s u m 1 sum1 sum1同问题2的 d d d, s u m 2 sum2 sum2也类似, l l l加上 l × x l\times x l×x, r + 1 r+1 r+1减去 ( r + 1 ) x (r+1)x (r+1)x
查询
单点 x x x的前缀和就是 ( x + 1 ) × s u m 1 (x+1)\times sum1 (x+1)×sum1中 x x x的前缀和- s u m 2 sum2 sum2中 x x x的前缀和,区间 [ l , r ] [l,r] [l,r]的值就是 r r r的前缀和- l − 1 l-1 l−1的前缀和
题目
Code
#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;}
小结
线段树与树状数组有很多相似的地方,但是树状数组很明显的优势就是短,但是线段树可以处理很多种情况,而这里面有些是树状数组做不到的,所以说不论是线段树还是树状数组,我们都应该学习一下,然后选择更好的去解决题目。
不定时更新高阶操作
发表评论
最新留言
关于作者
