树状数组学习笔记

前置知识: 前缀和。

前缀和即:用

\[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\n",sum(y)-sum(x-1)); //利用前缀和思想
	}
	return 0;
}

posted @ 2020-03-15 18:00  bifanwen  阅读(162)  评论(0)    收藏  举报