树状数组
树状数组的大小需要和数组里最大的元素一致
普通数组或者前缀和,要么查询是On,要么求和是On,于是就有了线段树和树状数组这两个东西。
他为啥快呢。
先说一下,树状数组的每个下标对应的数组内容都是其本身加上其子树的所有结点的和。
看上面这个图:
1只保存1
2保存1,2的
3只保存3的
4保存了1 2 3 4的。
优势1:区间求和
所以你想想是不是在求某一个区间的和的时候不像普通数组那样麻烦了,因为它本身就代表本身及其子树的和
优势2:修改节点
如果在修改某个结点的时候是不是也简单了,不像前缀和那样的On,只需要修改其本身,然后再顺着根节点往上一边爬一边修改直至整个树的根节点就好了。
所以他快。
但是他的快也仅仅局限于普通数组和前缀和数组的缺点,即查询区间和 以及修改结点
树状数组详细解释:
树状数组里,下标 i 对应的数是这个数所管辖的数的和。
但是他管多少似乎是不确定的按照这个树的画法来看。
但也不是无迹可寻。
lowbit(最低的比特位)
他管辖的元素的个数为 2^k,其中 k 是 i 转成二进制后最后一个1后面的0的个数。
只知道个数,不知道是谁吗?
也是可以推的。他管的元素肯定比他自己要小,还有数量的关系的话,从小到大推就应该能推出来。
2^k也是一个好东西,是lowbit(i)的值。因为我要知道一个数转化成二进制最后的1后面有几个0,所以你需要知道的是:
一个数的负数等于这个数的原码取反+1.
而这个数取反加1后,如果这个+1没有进位,那就只有最后一位一样,如果这个+1有进位,那就应该是原来的10100和取反+1后的01100的最后三位一样,也就是剩下100一样。100是4的二进制,而100后面正好有两个0,2^2=4,所以说明一个数逻辑与上他的负数,得到的就是2 ^k。
lowbit(i) = 2^k //k 是 i 转成二进制后最后一个1后面的0的个数。
题目在这儿
前面说过,普通的树状数组只在区间查询,修改单个节点上有优势,但是这个题是区间修改以及节点查询,这就有点和树状数组貌合神离了。
但也不是无药可救。
除了前缀和之外,还有一种特殊的数组加差分数组。
//假设a[i]是原数组,d[i]是差分数组
d[i] = a[i] - a[i-1]
也很简单很好理解,但是和树状数组放到一起就有点不太好理解了。
先说一下为什么要用差分数组
因为差分数组代表和前一个数的差,而且差分数组的第一个是减去的0,也就是原数组本身。所以如果把前 i 个数组加起来,就是第 i 个数本身了。
a[i] = d[1]+d[2]+... +d[i]
=a[1]+ a[2]-a[1] + a[3]-a[2] + ... a[i]-a[i-1]
=a[i]
所以查询用差分数组的话,查询结点只需要把前面所有的都加起来。而我们又知道树状数组区间查询是他的强项,因为他就是干这一行的(树状数组的每个节点代表本身加上其所有的子节点的和),所以这个用树状数组就很巧妙的把单个结点查询转移到了他的强项区间查询上
弄完了节点查询,还差区间修改。
我们知道,差分数组保存的是这个数和其前面的那个数的差值,试想,当我们将某个区间都加上某个值的话:
假设对 [3,5]区间每个都加上2(这里的3-5是数组的下标)
对差分数组来说,a[2]和a[3]的差距由原来的d[3]变成了d[3]+2; a[5]和a[6]的差距由原来的d[6]变成d[6]-2(因为a[5]变大了).所以差分数组只需要修改两个边界值,让差分数组里下标为2的数再加 2,下标为6的数-2.
那既然是用树状数组维护的,那就需要用树状数组的方式去改变这两个特殊的d[L]和d[r+1](L就是3,r就是5):
我只是修改了两个节点,所以又来到了树状数组的强项:结点修改。我不仅仅需要改自己,还需要修改我的父节点,父节点的父节点…
所以就完成了这个题了。
两种不同做法的小区别:
做法1
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 500001;
long long int A[N],C[N],sum[N];
int n,m;
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int k)
{
for(int i=x;i<=n;i+=lowbit(i))
sum[i]+=k;
}
void range_add(int l,int r,int k)
{
add(l,k);add(r+1,-k);
}
long long int find(int x) //查询第 x 个数
{
long long int res=0;
for(int i=x;i>=1;i-=lowbit(i))
res+=sum[i];
return res;
}
int main()
{
scanf("%d%d",&n,&m);
long long int now=0,last=0;
for(int i=1;i<=n;i++)
{
scanf("%lld",&now);
add(i,now-last);
last=now;
}
int choice;
long long int x,y,k;
while(m--)
{
scanf("%d",&choice);
if(choice==1)
{
scanf("%lld%lld%lld",&x,&y,&k);
range_add(x,y,k);
}
else if(choice==2)
{
scanf("%d",&x);
printf("%d\n",find(x));
}
}
}
做法2
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 500001;
long long int d[N],a[N]; //d[]表示差分数组,树状数组。a[]表示一开始输入的数组
int n,m;
int lowbit(int i)
{
return i&(-i);
}
void add(int x,int k)
{
for(int i=x;i<=n;i+=lowbit(i))
d[i]+=k;
}
void range_add(int x,int y,int k)
{
add(x,k);add(y+1,-k);
}
long long int query(int x)
{
long long int res=0;
for(int i=x;i>=1;i-=lowbit(i))
res+=d[i];
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
while(m--)
{
int t;
long long int x,y,k;
scanf("%d",&t);
if(t==1)
{
scanf("%lld%lld%lld",&x,&y,&k);
range_add(x,y,k);
}
else{
scanf("%lld",&x);
printf("%lld\n",query(x)+a[x]);
}
}
}
这两种做法的不同之处在于一个在一开始就按照add方法往差分数组里添加,另一个是在一个一开始全为0的数组里往里加或者是减,然后再加上原数组的对应的值。我认为第二种比较亲民一点。
有点不明白为啥是加原来的。
按照第一种做法做出来的的差分数组:
一开始得到的就不是差分数组,后面还能挽救?
是树状数组的特性,改了孩子结点,就要修改父亲节点。大同小异!