其他分享
首页 > 其他分享> > 21.3.2 树状数组 总结

21.3.2 树状数组 总结

作者:互联网

树状数组

特点

树状数组 \(c[x]\) 维护序列 \(a\) 的区间 \([x - lowbit(x) + 1 , x]\)

lowbit(x) = x & -x;

性质 (摘自小蓝书)

  1. \(c[x]\)保存以它为根的子树中所有叶节点的信息和
  2. \(c[x]\)的儿子节点(含它本身)的个数等于 \(lowbit(x)\) 后的位数。
  3. 除根节点外,每个内部节点 \(c[x]\) 的父节点是 \(c[x + lowbit(x)]\)
  4. 树的深度为 \(O(logN)\)

基操

  1. 单点修改
  2. 单点查询

代码

单点修改

void add(int x,int y) {
	for(; x <= n; x += x & -x) c[x] += y;
	//x + lowbit(x)遍历的是x的父节点,利用了性质3
}

单点查询

int ask(int x) {
	int ans = 0;
	for(; x; x -= x & -x) ans += c[x];
	return ans; 
	//x - lowbit(x)遍历的是x的兄弟节点,结合上图理解
}
推荐练习

洛谷板子题

升级操作

逆序对

逆序对问题简单来说就是求形如 \(i < j\) 且 \(a[i] > a[j]\) 的个数。

在序列 \(a\) 的数值范围上建立树状数组,初始化全为零。
倒序扫描给定的序列 \(a\) ,对于每个数 \(a[i]\):

  1. 在树状数组中查询前缀和 \([1,a[i] - 1]\) ,累加到答案中。
  2. 执行单点修改操作时,把 \(c[a[i]]\) 的值 \(+1\),表示 \(a[i]\) 又出现了一次同时维护前缀和。
int ans = 0;
for(int i = n; i; i--) {
	ans += ask(a[i] - 1);
	add(a[i],1);
}

如果数值范围过大可以对数据进行离散化,注意值域需为正数。

原理

在倒序扫描时,已经出现过的数就是在 \(a[i]\) 后面的数,所以树状数组查询的内容就是在 \(a[i]\) 后比它小的数有多少个,与问题等价。

推荐练习

楼兰图腾

注意数据范围

#include<cstdio>
#include<cstring>
const int N = 2e5;
int left[N + 5],right[N + 5],c[N + 5],a[N + 5],n;
void add(int x,int y) {
	for(; x <= N; x += x & -x) c[x] += y;
}
int ask(int x) {
	int ans = 0;
	for(; x; x -= x & -x) ans += c[x];
	return ans;
}
inline int read() {
	int x = 0,flag = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-')flag = -1;ch = getchar();}
	while(ch >='0' && ch <='9'){x = (x << 3) + (x << 1) + ch - 48;ch = getchar();}
	return x * flag ;
}
long long ans = 0;
int main() {
	n = read();
	for(int i = 1; i <= n; i++) a[i] = read();
	for(int i = 1; i <= n; i++) {
		left[i] = ask(N) - ask(a[i]);
		add(a[i],1);
	}
	memset(c,0,sizeof(c));
	for(int i = n; i >= 1; i--) {
		right[i] = ask(N) - ask(a[i]);
		add(a[i],1);
	}
	for(int i = 1; i <= n; i++) ans += 1ll * left[i] * right[i];
	printf("%lld ",ans);
	memset(c,0,sizeof(c));
	ans = 0;
	for(int i = 1; i <= n; i++) {
		left[i] = ask(a[i] - 1);
		add(a[i],1);	
	}	
	memset(c,0,sizeof(c));
	for(int i = n; i >= 1; i--) {
		right[i] = ask(a[i] - 1);
		add(a[i],1);
	}
	for(int i = 1; i <= n; i++) ans += 1ll * left[i] * right[i];
	printf("%lld",ans);	
	return 0;
}
区间增加 + 单点查询

思路:变区间增加为单点增加。
维护一个新数组 \(b\) 的前缀和,\(b[1,x]\) 表示 \(a[x]\) 的变化量。
将 \(C\) \(l\) \(r\) \(d\) 变为 \(b[l]\) 加上 \(d\),\(b[r + 1]\) 减去 \(d\)。
原理很好想,就不赘述了。
查询答案时将查询到的值加上原值就可以了。
这种把维护区间转换成维护区间变化的思路特别重要。

推荐练习

一个简单的整数问题

#include<cstdio>
const int N = 1e5 + 5;
int a[N],c[N],n,m;
void add(int x,int y) {
	for(; x <= n; x += x & -x) c[x] += y;
}
int ask(int x) {
	int ans = 0;
	for(; x; x -= x & -x) ans += c[x];
	return ans;
}
int main() {
	scanf("%d%d",&n,&m);
	for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
	for(int i = 1; i <= m; i++) {
		char ord[2];
		scanf("%s",ord);
		if(ord[0] == 'C') {
			int l,r,d; scanf("%d%d%d",&l,&r,&d);
			add(l,d);
			add(r + 1,-d);
		}
		else {
			int x; scanf("%d",&x);
			printf("%d\n",a[x] + ask(x));
		}
	}
	return 0;
}
区间增加 + 区间查询

详见小蓝书(算法竞赛进阶指南)P207 ~P209。

推荐练习

一个简单的整数问题2

#include<cstdio>
const int N = 1e5 + 5;
int a[N],n,m;
long long c[2][N],sum[N];
void add(int i,int x,int y) {
	for(; x <= n; x += x & -x) c[i][x] += y;
}
long long ask(int i,int x) {
	long long ans = 0;
	for(; x; x -= x & -x) ans += c[i][x];
	return ans; 
}
int main() {
	scanf("%d%d",&n,&m);
	for(int i = 1; i <= n; i++) {
		scanf("%d",&a[i]);
		sum[i] = sum[i - 1] + a[i];
	}
	for(int i = 1,l,r; i <= m; i++) {
		char ord[2];
		scanf("%s%d%d",ord,&l,&r);
		if(ord[0] == 'C') {
			int d; scanf("%d",&d);
			add(0,l,d);
			add(0,r + 1,-d);
			add(1,l,l * d);
			add(1,r + 1,-(r + 1) * d);
		}
		else {
			long long ans = sum[r] + (r + 1) * ask(0,r) - ask(1,r);
			ans -= sum[l - 1] + l * ask(0,l - 1) - ask(1,l - 1);
			printf("%lld\n",ans);
		}
	}
	return 0;
}

推荐练习

由易到难,做完所有的推荐练习,你就能熟练掌握树状数组了。

板子题1
板子题2
最接近神的人(一道阅读理解题,可以借这道题练一下“值域树状数组”的离散化。)
谜一样的牛
方差(虽然这道题线段树更好写,但可以尝试一下树状数组的写法。)

尾声

树状数组作为进阶数据结构中较为简单的结构之一,其作用更多的是帮助维护其它数据结构。

标签:单点,21.3,树状,int,查询,add,数组
来源: https://www.cnblogs.com/sjzyh/p/14826245.html