21.3.2 树状数组 总结

树状数组

特点

树状数组 c [ x ] c[x] c[x] 维护序列 a a a 的区间 [ x − l o w b i t ( x ) + 1 , x ] [x - lowbit(x) + 1 , x] [xlowbit(x)+1,x]

lowbit(x) = x & -x;

性质 (摘自小蓝书)

  1. c [ x ] c[x] c[x]保存以它为根的子树中所有叶节点的信息和
  2. c [ x ] c[x] c[x]的儿子节点(含它本身)的个数等于 l o w b i t ( x ) lowbit(x) lowbit(x) 后的位数。
  3. 除根节点外,每个内部节点 c [ x ] c[x] c[x] 的父节点是 c [ x + l o w b i t ( x ) ] c[x + lowbit(x)] c[x+lowbit(x)]
  4. 树的深度为 O ( l o g N ) O(logN) 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 i < j i<j a [ i ] > a [ j ] a[i] > a[j] a[i]>a[j] 的个数。

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

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

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

原理

在倒序扫描时,已经出现过的数就是在 a [ i ] a[i] a[i] 后面的数,所以树状数组查询的内容就是在 a [ i ] 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 b 的前缀和, b [ 1 , x ] b[1,x] b[1,x] 表示 a [ x ] a[x] a[x] 的变化量。
C C C l l l r r r d d d 变为 b [ l ] b[l] b[l] 加上 d d d b [ r + 1 ] b[r + 1] b[r+1] 减去 d d 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
最接近神的人(一道阅读理解题,可以借这道题练一下“值域树状数组”的离散化。)
谜一样的牛
方差(虽然这道题线段树更好写,但可以尝试一下树状数组的写法。)

尾声

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值