树状数组详解(入门)

引入:

树状数组二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为Fenwick树,最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和, 区间和。——百度

说人话:树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构。

什么是单点修改?答:给定x,y,让a[x]自增y

什么是区间查询?答:给定l,r,求a[l....r]的合

类似地,还有:「区间修改」、「单点查询」。

原理:

树状数组能快速求解信息的原因:我们总能将一段前缀[1,n] 拆成 不多于logn段区间,使得这logn段区间的信息是 已知的

举个例子:我们要求a[1.....7]的前缀合怎么办?

最暴力的方法就是计算a[1]+a[2]+...+a[7],但是这要算7次,有没有更好的方法?——当然有(于是树状数组闪亮登场。。。可能还有线段树)

如果我告诉你三个数X,Y,Z,且X=a[1....4]的合,Y=a[5....6]的合,Z=a[7...7]的合(其实就是a[7])

此时你会怎么算?你一定会回答:X+Y+Z(因为这样只需算3次)

小结:我们只需合并这logn 段区间的信息,就可以得到答案。相比于原来直接合并n个信息,效率有了很大的提高。

下面这张图展示了树状数组的工作原理:

我们首先引入二叉树,叶子节点代表A[1]~A[8]。

现在变形一下:

  现在加上上面讲的原理再理解树状数组的操作原理:

c数组表示就是用来储存原始数组a某段区间的和的,也就是说,这些区间的信息是已知的,我们的目标就是把查询前缀拆成这些小区间。

例如,从图中可以看出:

  • c[1]表示a[1...1]的合
  • c[2]表示a[1...2]的合
  • c[3]表示a[3...3]的合
  • c[4]表示a[1...4]的合
  • c[5]表示a[5...5]的合
  • c[6]表示a[5...6]的合
  • c[7]表示a[7...7]的合
  • c[8]表示a[1...8]的合 
  • c[x]表示a[M...x]的合(其中M,即左端点我们还不知道,但是x,即右端点我们知道就是x)

不难发现,c[x]管辖的一定是一段右边界是x的区间的信息。我们先不关心左边界,先来感受一下树状数组是如何查询的。

过程:从 c[7]开始往前跳,发现 c[7]只管辖 a[7] 这个元素;然后找c[6],c[6]发现c[6] 管辖的是 a[5...6],然后跳到 c[4],发现 c[4] 管辖的是 a[1...4] 这些元素,然后再试图跳到 c[0],但事实上 c[0]不存在,不跳了。

我们刚刚找到的c是c[7],c[6],c[4],其实c[7]+c[6]+c[4]就是a[1...7]的合啦~~~QAQ

举例:计算a[4...7]的合

其实求a[4...7],其实就是查询a[1...7]的合与a[1...3]的合,最终将两个结果作差。

 管辖区间:

那么问题来了,c[x](x>=1)管辖的区间到底往左延伸多少?也就是说,区间长度是多少?

树状数组中,规定 c[x]管辖的区间长度为 2^{k},其中:

  • 设二进制最低位为第 0 位,则 k恰好为 x二进制表示中最低位的1 所在的二进制位数;
  • 2^{k},即c[x]的区间长度恰好为x在二进制表示中,最低位的 1 以及后面所有 0 组成的数。

 举个栗子:c[88]所管辖的区间

因为88的十进制为88,二进制为01011000,其二进制最低位的 1 以及后面的 0 组成的二进制是 1000,所以c[88]所管辖的区间长度为二进制数1000转为十进制数的结果,即8。

因此c[88]管辖a[81...88]的合。

我们记 x 二进制最低位 1 以及后面的 0 组成的数为 lowbit(x)。

那么 c[x]管辖的区间就是 a[x-lowbit(x)+1,x];

这里注意:lowbit指的不是最低位 1 所在的位数 k,而是这个 1 和后面所有 0 组成的2^{k}

怎么计算 lowbit?根据位运算知识,可以得到 lowbit(x) = x & -x

lowbit的原理:

将 x 的二进制所有位全部取反,再加 1,就可以得到 -x 的二进制编码。例如, 的二进制编码是 110,全部取反后得到 001,加 1 得到 010

设原先 x 的二进制编码是 (...)10...00,全部取反后得到 [...]01...11,加 1 后得到 [...]10...00,也就是 -x 的二进制编码了。这里 x 二进制表示中第一个 1 是 x 最低位的 1

(...) 和 [...] 中省略号的每一位分别相反,所以 x & -x = (...)10...00 & [...]10...00 = 10...00,得到的结果就是 lowbit

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

 区间查询:

实现时,我们不一定要先把 c都跳出来然后一起合并,可以边跳边合并。  

int get_sum(int x){// a[1]..a[x]的和 
	int ans=0;
	while(x>0){
		ans+=c[x];
		x-=lowbit(x);
	}
	return ans;
}

 单点修改:

现在来考虑如何单点修改 a[x]。

当我们修改A数组中某个值时,应当如何更新C数组呢?回想一下,区间查询的过程,再看一下上文中列出的过程。这里声明一下:单点更新实际上是不修改A数组的,而是修改树状数组C,向上更新区间长度为lowbit(i)所代表的节点的值。

当在A[1]加上值k,即更新A[1]时,需要向上更新C[1],C[2],C[4],C[8],这个时候只需将这4个节点每个节点的值加上k即可。

 下面以维护区间和,单点加为例给出实现。

void add(int x,int k){
	while(x<=n){// 不能越界
		c[x]+=k;
		x+=lowbit(x);
	}
} 

 总结:

树状数组的重点就是利用二进制的变化,动态地更新树状数组。

树状数组的每一个节点并不是代表原数组的值,而是包含了原数组多个节点的值。

所以在更新a[1]时需要将所有包含a[1]的c[i]都加上k这也就利用到了二进制的神奇之处。

如果是更新a[i]的值,则每一次对c[i] 中的 i 向上更新,即每次i+=lowbit(i),这样就能c[i] 以及c[i] 的所有父节点都加上k。

反之求区间和也是和更新节点值差不多,只不过每次 i-=lowbit(i)。

树状数组&&线段树

区别:

1.两者在复杂度上同级, 但是树状数组的常数明显优于线段树, 其编程复杂度也远小于线段树.

2.树状数组的作用被线段树完全涵盖, 凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决.

3.树状数组的突出特点是其编程的极端简洁性, 使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,其代码效率远高于线段树。

最后一点BB:

本篇文章有借鉴oi.wiki和https://blog.csdn.net/ls2868916989/article/details/119268741

十分感谢!

若想继续提高有关树状数组的知识,详见树状数组提高篇

东方欲晓,莫道君行早。踏遍青山人未老,风景这边独好。加油啊!!!,拜拜~~~QAQ

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值