树状数组学习笔记

前置知识: 前缀和。

前缀和即:用

[s_i = sum_{j=1}^i a_j ]

然后

[sum_{i=l}^r a_i = s_r - s_{l-1} ]

树状数组是基于前缀和的一种数据结构。

它主要可以维护区间和、单点改、区间改、单点查。

和线段树相比,码量较少、空间复杂度较少是一大优势;但功能不全(比如区间排名,这权值线段树可以搞,但树状数组无法实现)也是弱点。

说实话,前人的智慧是无穷的,能想到树状数组这种超能的武器!

树状数组的原理:

每一个节点存储以自己结尾的若干节点的和。那么“若干节点”是多少个呢?下面考虑一个 lowbit.

inline int lowbit(int x) {
	//求2^{x末尾0的个数} 
	return x & (-x);
}

至于它怎么求的,你不用管;只需要知道这是求什么就行,代码中注释了。

那么,看个图:

你会发现,如果 (S=lowbit(x))

此时 (x) 的紧靠着左边的那个节点就是 (x-S)

紧靠着右边的那个节点就是 (x+S) (这其实也是自己的父亲)

然后呢,查询 (1) ~ (n) 的和只需要这几步:

  1. 找出 (lowbit(n))的值

  2. 然后 (n) 变为自己紧靠着的左边的那个节点

  3. 如果 (n leq 0) 结束;否则回到第一步。

(代码中 (c) 数组就是树状数组)


inline ll sum(int x) {
	//求n的前缀和
	ll s=0; while(x>0) {
		s+=c[x];
		x-=lowbit(x);
	//这是x节点紧贴着的前一个区间 
	} return s;
}

那么,我们需要知道:怎么建树呢?

如果想建树,不外两种:

  1. 不断单点修改

  2. 强行构树(比如线段树)

我们要考虑如何单点修改。

其实不难,做三步:

  1. 找到 (lowbit(n)) 并暴力加

  2. 然后 (n) 变为自己紧靠着的右边的那个节点

  3. 如果 $x > n $ 结束;否则回到第一步。

这和求和差不多。一个往前,一个往后,异曲同工。

下面,我们可以解决第一个模板。

树状数组1

#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;

const int N=1e6+1;
typedef long long ll;

inline int read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
	int x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}

int a[N]; ll c[N];
int n,m,x,y;

inline int lowbit(int x) {
	//求2^{x末尾0的个数} 
	return x & (-x);
}

inline ll sum(int x) {
	//求n的前缀和
	ll s=0; while(x>0) {
		s+=c[x];
		x-=lowbit(x);
	//这是x节点紧贴着的前一个区间 
	} return s;
}

inline void update(int x,int k) {
	a[x]+=k; while(x<=n) {
		c[x]+=k; //暴力加和 
		x+=lowbit(x); 
	//这是x节点紧贴着的后一个区间 
	}
}

int main(){
	n=read(); m=read();
	for(int i=1;i<=n;i++) {
		x=read(); update(i,x);
	} while(m--) {
		int opt=read(); x=read(),y=read();
		if(opt==1) update(x,y); //单点修改
		else printf("%lld
",sum(y)-sum(x-1)); //利用前缀和思想
	}
	return 0;
}

简易的代码胜过复杂的说教。
原文地址:https://www.cnblogs.com/bifanwen/p/12498948.html