树状数组 从零基础到入门

简述

树状数组是个很强大的数据结构,主要用于对数组的单点/区间修改和查询,两种操作时间复杂度均为O(logn)。

为什么叫树状数组呢,因为它长得像右对齐的二叉树,如图。

树状数组 从零基础到入门_第1张图片创造树状数组的神,应用二进制思想,为这课二叉树的每一个结点赋值:

树状数组 从零基础到入门_第2张图片

令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个元素的和,则:

树状数组 从零基础到入门_第3张图片

 通过上面的公式发现,要进行区间查询操作就要记录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;
}

完结撒花,终于入门了。

推下姊妹篇:线段树 从零基础到入门

你可能感兴趣的:(算法,c++)