树状数组详解(附例题)

树状数组

一个包含n个数的序列2,7,1,125,…计算前i个数的和值,即前缀和query[i]=a[1]+a[2]…+a[i] (1,2…n) 。该怎么计算呢?

累加求前n个数的和值需要O(n)时间。而且若对a[i]进行修改,则query[i],query[1]… query[m]都需要修改,最坏的情况下需要O(n)时间。

树状数组可以高效实现,其查询前缀和与点更新均为O(logn)。那么树状数组是如何巧妙地实现呢?

树状数组引入了分级管理制度,设置一个管理小组,每个管理员管理
一个或多个连续的元素。例如,数列有9个元素,分别用a[1],a[2]…a[9]存储,管理数组为c[]。管理数组c[]是树状的,因此称为树状数组
在这里插入图片描述

在这里插入图片描述

树状数组,又称为二进制索引树(Binary Indexed Trees),通过二进制分解划分区间。那么c[i]存储的是哪些值?

1、区间长度

若i的二进制表示末尾有k个连续的0,则e[i]存储的区间长度为 2 k 2^k 2k,从a[i]向前数 2 k 2^k 2k个元素,即 c [ i ] = a [ i − 2 k + 1 ] + a [ i − 2 k + 2 ] + . . . + a [ i ] 。 c[i]=a[i-2^k+1]+a[i-2^k+2]+...+a[i]。 c[i]=a[i2k+1]+a[i2k+2]+...+a[i]

在这里插入图片描述

区间长度就是i的二进制表示下最低位的1及它后面的0构成的数值。例如i=20,其二进制表示为10100,末尾有2个0,区间长度为 2 2 2^2 22,其实就是10100最低位的1及其后面的O构成的数值 ( 100 )   2 (100)~_2 (100) 2,十进制为4。

计算区间长度的方法为 i & ( ( ∼ i ) + 1 ) i\&((\sim i)+1) i&((i)+1)。在计算机中二进制数采用的是补码表示,-i的补码正好是i取反加1。所以c[i]存储的区间长度: l o w b i t ( i ) = ( − i ) & i lowbit(i)=(-i) \& i lowbit(i)=(i)&i

2、前驱和后继

直接前驱:c[i]的直接前驱为c[i-lowbit(i)],即e[i]左侧紧邻的子树的根。

直接后继:c[i]的直接后继为c[i+lowbit(i)],即c[i]的父节点。

前驱: c[i]左侧所有子树的根。

后继: c[i]的所有祖先。

在这里插入图片描述

3、查询前缀和

前i个元素的前缀和query[i]等于c[i]加上c[i]的前驱。如:
query[7]等于c[7]加上c[7]的前驱,query[7]=c[7]+c[6]+c[4]。

4、点更新

若对a[i]进行修改,令a[i]加上一个数z,则只需更新c[i]及其后继(祖先),即令这些节点都加上z即可,无需修改其他节点。
例如,修改a[5],令其加2。只需c[5]+2,然后c[5]的后继分别加2,即c[6]+2、c[8]+2。

5、查询区间和

若求区间和值a[i]+a[i+1]+ …+a[j],则求解前j个元素的和值减去前i-1个元素的和值即可,即query[j]-query[i-1]。
s u m [ j ] − s u m [ i − 1 ] = a [ i ] + a [ i + 1 ] + . . . + a [ j ] sum[j]-sum[i-1]=a[i]+a[i+1]+...+a[j] sum[j]sum[i1]=a[i]+a[i+1]+...+a[j]
算法分析:
点更新时,从叶子更新到树根,执行的次数不超过树的高度O(logn)。
前缀和查询时,从当前结点一直查找前驱,前驱的个数不超过O(logn)。

在这里插入图片描述

树状数组的局限性

树状数组主要用于查询前缀和区间和及点更新,对点查询、区间修改效率较低。

前缀和:求a[1]…a[i]的前缀和,普通数组O(n),树状数组O(logn)。

区间和:求a[i]…a[j]的区间和,普通数组O(n),树状数组O(logn)。

点更新:修改a[i]加上z,普通数组O(1),树状数组O(logn)。

点查询:查找第i个元素,普通数组O(1),树状数组O(logn) (求query[i]-query[i-1])。

区间更新:区间a[i]…a[j]的所有元素加z,普通数组O(n),树状数组O(nlogn) 。

代码

#include <bits/stdc++.h>
#define N 100
using namespace std;
int c[N];

int lowbit(int i) {
	return i&(-i);
}

//前缀和
int query(int i) {
	if(i) return c[i]+query(i-lowbit(i));
	return 0;
}

// 区间和
int query(int s,int e) {
	return query(e)-query(s-1);
}

// 点更新,c[i]加上z
void update(int i,int z,int n) {
	for(; i<=n; i+=lowbit(i)) c[i]+=z;
}

int main() {
	int a[10]= {1,2,3,4,5,6,7,8,9};
	int n=sizeof(a)/sizeof(a[0]);
	//生成树状数组
	for(int i=1; i<=n; i++) update(i,a[i-1],n);
	return 0;
}

例题

1409. 查询带键的排列 - 力扣(Leetcode)

1395. 统计作战单位数 - 力扣(Leetcode)

2250. 统计包含每个点的矩形数目 - 力扣(Leetcode)

2424. 最长上传前缀 - 力扣(Leetcode)

327. 区间和的个数 - 力扣(Leetcode)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白沐沐vccc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值