目录
0.前言
先简单介绍一下树状数组:
树状数组,是一种修改和查询都为时间复杂度的数据结构,常用于数组的多次修改与访问。
树状数组就是由完全二叉树减去一部分节点形成的,类似于前缀和,但是其思想又超越了前缀和,故此具有更好的时间复杂度与更高的难度。
(加点官方资料提升档次)
记住,待会儿要讲!
树状数组的结构示意图
1.lowbit
首先,我们要讲一讲lowbit这东西。lowbit,翻译成人话就是最后的比特。何为最后的比特?就像这样——
=
lowbit,就是一个数字2进制中从右往左数第一个1及其右边的0所构成的二进制数字。
那lowbit怎么求呢?(还能怎么求,用for循环一位一位看呗)
Wrong Answer ↑
正确操作:x&(-x)
(?)
(小朋友,你是否有很多问号?)
求lowbit的原理解释:
首先明确一点:当一个负数参加与运算时,会将其转换为补码
当 -x 转换为补码时,末尾全部的0变成1,最后的1变成0。再加1之后末尾所有的1(原来的0)都进位为0,直到碰见一个0(原来的1),就不会再进位,该位变成1。所以最低位到最右边的1的部分都不会变。而在其之左的部分因为取反,进行与运算后都会变成0。这就是lowbit的原理。
那lowbit有什么用
呢?
不要着急,慢慢来看——
2.树状数组的向上推导
树状数组的向上推导,常用于单点修改。每次修改的时间复杂度为。为何?看图便知——
以101(二进制)举例,每次向上推导时都会提高一个“层次”,且每个“层次”都只会访问一个节点。仔细观察不难发现,自下而上数第一层节点个数为四,第二层节点数为二,第三层节点数为一——随着层次增高,每层的节点个数以指数数量减少。因此,每一次向上推导都是级别的。
向上推导的原理?
那每次具体加多少呢?聪明的你想必已经猜到——lowbit。那为什么是lowbit呢?
(先思考,再看下文)
使用数学归纳法就可以发现每次加上lowbit是正确的。当然,我们也可以推理出结论。众所周知,二进制逢二进一。还是以101举例,101其实就是1*+0*+1*。而树状数组和其有异曲同工之妙——每两个相同长度的部分数组,都会合并成一个更大、层数更高的部分数组。(实际中还要看位置关系)(想一想为什么这两个部分数组高度一定相同)于是,向上推导的过程就变成了一个把(n-i)拆分成若干个2的自然数次方的和的形式。为什么这个拆分会和lowbit有关呢?想一想,在某一位上缺1,那么这一位上一定就是1。既然我们是每次从小到大增加2的自然数次方,那么增加的自然就是lowbit。同时,这也从另一种角度解释了为什么向上推导的时间复杂度为——在极端情况下,(指原数约等于1)从1加到要加n次,对于来说就是log级别。
void ad(int x, int k) {
while (x <= n) {
tree[x] += k;
x += lowbit(x);
}
}
3.树状数组的向下推导
其实向上推导和向下推导是相通的。向上推导主要应用于单点修改,而向下推导则主要应用于求[1,x]的区间和(哎,终于有点正常数组的样子了)只要理解了向上推导,就一定能理解向下推导。 废话少说,先放图——
向下推导的原理?
如图,从1000出发,那么[1,1000]的区间和就是+++。同样的,我们把1000前面的大小为七的原数组拆分成2的自然数次方的形式:7=++。于是,通过同样的操作,我们一步步减去lowbi,一步步求和,直到减为0为止,求和也就用的时间复杂度给解决了。
4.树状数组求区间和
在前言中已经介绍过,树状数组常用于数组的多次修改与访问,且类似于前缀和(本人观点)。那树状数组是如何求区间和的呢?下面让我们来一探究竟。
回忆一下前缀和求区间值。比如[l,r]=sum[r]-sum[l-1]。那树状数组是不是一样的呢?答案是大同小异。运用我们刚刚学会(各位dalao给个面子配合一下)的向下推导,可以得到如下伪代码——
int getsum(int x) {
int sum = 0;
while (x) {
sum += tree[x];
x -= lowbit(x);
}
return sum;
}
关于求特定区间,使用两次查询再作差即可,时间复杂度依然是。
下面就是完整代码:
#include<iostream>
#define int short
#define lowbit(x) x&(-x)
using namespace std;
int n, m; //数组长度 操作次数
int tree[100005];
void ad(int x, int k) {
while (x <= n) {
tree[x] += k;
x += lowbit(x);
}
}
int getsum(int x) {
int sum = 0;
while (x) {
sum += tree[x];
x -= lowbit(x);
}
return sum;
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
ad(i, x);
}
while (m--) {
int flag;
cin >> flag;
if (flag == 0) {//操作0:把a[x]增加k
int x, k;
cin >> x >> k;
ad(x, k);
} else {//操作1:求出区间[l,r]的和
int l, r;
cin >> l >> r;
cout << getsum(r) - getsum(l - 1) << endl;
}
}
}
//代码动了点小手脚,不要无脑抄袭!
!完结撒花!
为了试炼一下手感,写几道题目吧!