树状数组是个很强大的数据结构,主要用于对数组的单点/区间修改和查询,两种操作时间复杂度均为O(logn)。
为什么叫树状数组呢,因为它长得像右对齐的二叉树,如图。
创造树状数组的神,应用二进制思想,为这课二叉树的每一个结点赋值:
令A为原始数组,C为我们建立的树状数组,令C存储它所有子节点的和,则有:
C1=A1,1(10)=1(2)
C2=C1+A2=A1+A2,2(10)=10(2)
C3=A3,3(10)=11(2)
C4=C2+C3+A4=A1+A2+A3+A4,4(10)=100(2)
……
我们发现,若i的二进制表示中含n个0,则Ci中存放2^n个原始数据A。
那么当我们输入一个数Ai或将数组中第i个数加x(其实都一样)时,如何更新树状数组呢?显然只需找到i上方的若干个父节点并将其更新。
根据树状数组存放特点,当i<=数组长度n时,将i的二进制每次取最末位的1,加上这个末位1的权值即为C数组中存放它的位置。
举个栗子,当n等于8,对于A2,首先当然要更新C2,接着取最末位1的权值2进行第一次相加得4,再取4最末位1的权值4进行第二次相加得8,即当A2更新C2、C4、C8也要更新,符合上图。
创造树状数组的神用极致简短的lowbit函数实现了取末位1的权值功能:
inline int lowbit(int x){return x&(-x);}
原理很简单,计算机中负数以补码形式存储。补码即反码加1,将原码与补码进行按位与操作即可求出最末位1的权值(建议手导)。
所以对于某一个点的修改,只需一个循环:
inline void add(int x,int k){//x为修改点的位置,k为修改的值
for(int i=x;i<=n;i+=lowbit(i)) c[i]+=k;
}
然后是求前缀和,即找到若干个C,使其不重复地包含A1~Ai,同样能看出对i的二进制每次减去末位1的权值,将结果加Ci直至i为0。
再举个栗子,当求6的前缀和时,由于6(10)=110(2),首先还是取自己,即C6=A5+A6,然后取末位1的权值2,6-2=4,则取C4=A1+A2+A3+A4,再取末位权值4,4-4=0,跳出循环,所求结果即C6+C4=A1+A2+A3+A4+A5+A6。
inline int ask(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=c[i];
return res;
}
传送门
非常模板的题,区间[l,r]的和即为r的前缀和减去l-1的前缀和。区间
#include
using namespace std;
inline int read(){
register int x=0,f=1;
register char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=1e6+5;
int n,q,a[N],c[N];
inline int lowbit(int x){return x&(-x);}
inline void add(int x,int k){
for(int i=x;i<=n;i+=lowbit(i)) c[i]+=k;
}
inline int ask(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=c[i];
return res;
}
int main(){
n=read(),q=read();
for(int i=1;i<=n;i++) a[i]=read(),add(i,a[i]);
int qwq,x,y;
while(q--){
qwq=read(),x=read(),y=read();
if(qwq==1){
add(x,y);
}
else{
printf("%d\n",ask(y)-ask(x-1));
}
}
return 0;
}
传送门
很容易想到类比单点修改区间查询,只要在修改[l,r]中元素时枚举每一个点进行修改即可。恭喜你,TLE警告。
这时发现了一个东西叫差分,顾名思义,数组中存储当前元素和前一个元素的差,例如对于数组a[]={1,5,4,2,6},差分数组即为c[]={1,4,-1,-2,4}。
根据差分数组的性质,查询第i个元素只需求差分数组中元素1~i的和,这是树状数组擅长的操作。
而对[l,r]进行区间修改时,只需使c[l]+x,c[r+1]-x即可。麻麻再也不担心我TLE
当然,读入的时候也要存储和前一个元素的差。
#include
//#define int long long
using namespace std;
inline int read(){
register int x=0,f=1;
register char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=5e5+5;
int n,m,c[N];
inline int lowbit(int x){return x&(-x);}
inline void add(int k,int x){
for(int i=x;i<=n;i+=lowbit(i)) c[i]+=k;
}
inline int ask(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=c[i];
return res;
}
int main(){
n=read(),m=read();
int now,pre=0;
for(int i=1;i<=n;i++) now=read(),add(now-pre,i),pre=now;
int qwq,x,y,k;
while(m--){
qwq=read(),x=read();
if(qwq==1){
y=read(),k=read();
add(-k,y+1),add(k,x);
}
else printf("%d\n",ask(x));
}
return 0;
}
找不到传送门
依旧是差分的思想。
公式编辑器有点问题,浅用一下截图。设b[n]为前n个元素的和,则:
通过上面的公式发现,要进行区间查询操作就要记录c[i]*i的前缀和。于是可以同时维护c[i]和c[i]*i两个树状数组来O(logn)实现区间查询。
#include
using namespace std;
inline int read(){
register int x=0,f=1;
register char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=1e6+5;
int n,q,c[N],d[N];
inline int lowbit(int x){return x&(-x);}
inline void add(int x,int k){
for(int i=x;i<=n;i+=lowbit(i)) c[i]+=k;
}
inline void addsum(int x,int k){
for(int i=x;i<=n;i+=lowbit(i)) d[i]+=k;
}
inline int ask(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=c[i];
return res;
}
inline int asksum(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=d[i];
return res;
}
signed main(){
n=read(),q=read();
int now,pre=0;
for(int i=1;i<=n;i++){
now=read();
add(i,now-pre);
addsum(i,(now-pre)*i);
pre=now;
}
int qwq,l,r,x;
while(q--){
qwq=read(),l=read(),r=read();
if(qwq==1){
x=read();
add(l,x),add(r+1,-x);
addsum(l,x*l),addsum(r+1,-x*(r+1));
}
else{
int sum1=ask(l-1)*l-asksum(l-1);
int sum2=ask(r)*(r+1)-asksum(r);
printf("%d\n",sum2-sum1);
}
}
return 0;
}
完结撒花,终于入门了。
推下姊妹篇:线段树 从零基础到入门