树状数组-讲解

树状数组

前言:这是令我印象最深的数据结构,曾经和自己的队友们一起苦苦地攻克这块的内容,一遍又一遍地敲板子去理解。怀念那时的时光......

1.树状数组概述

什么是树状数组:树状数是一种轻量级的数据结构,可以用来解决区间的动态查询修改问题。    
从功能上来讲,树状数组有三个类型的功能

 1. 单点修改区间查询
 2. 区间修改区间查询
 3. 区间修改区间查询
 

操作描述复杂度数组复杂度
建树初始化线段树O(n)木有
单点修改修改某一个点的值O(logn)O(1)
单点查询查询某一个点的值O(logn)O(1)
区间修改对一段连续区间做相同的修改O(logn)O(n)
区间查询查询一段连续区间数值的和O(logn)O(n)

 

单点修改区间查询 

首先咱们从简单的开始说起。

问题就是:给出一段长度为n的序列:a1,a2,...,an ,对其进行了m次操作,操作共有两种类型:

按照惯例,我们还是先考虑一下朴素(暴力)的解法👇

暴力就非常地直接,说啥做啥就可以,那么对于每个add操作就直接a[x] + =d即可,对于每个query操作就枚举[l,r]这段区间里所有a的和即可

这样的时间复杂度是O(nm),代码也比较好写👇

void add(int x,int d){
    a[x]+=d;
}
int query(int l,int r){
    int ret=0;
    for(int i=l;i<=r;i++)
        ret+=a[i];
    return ret;
}

暴力之后向大家介绍一种方法

前缀和优化

前缀和这种想法自然就是考虑到了add这种操作比较少(好些),但query操作很多还很复杂。具体做法就是维护一个数组s,要满足

s[i] = \sum ij=1   = a[j]  (存前缀)那么对于query查询操作,s[r] - s[l - 1]就是答案 。但是如果在add操作时会改动许多数,对这个s数组的影响也是非常大的,也因此这种方法的困难就在这。

void add(int x, int d) {
    a[x] += d;
    for (int i = x; i <= n; i++)
        s[x] += d;
}
long long query(int l, int r) {
    return s[r] - s[l - 1];
}

进入正题👇

树状数组的做法

本质:二进制拆分

刚才舍弃了前缀和做法的原因就是add有点多还复杂。而树状数组则运用了一种巧妙的,高效的,动态的实现了前缀和的查询和修改。

当然要说树状数组是个啥?那肯定是个数组啊!一般来讲我们设这个数组为C。

首先介绍一下树状数组中重要的lowbit 操作下

Lowbit

lowbit(i) 为i的二进制表示中,最低位的1对应2的幂,举几个栗子👇

数字二进制最低为的1lowbit
71111lowbit(7)=1
14111010lowbit(14)=2
99611 1110 0100100lowbit(996)=4
102410000000001000000000lowbit(1024)=1024

求Lowbit

至于怎么求出这个lowbit?先亮代码👇

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

lowbit 表示的是这个数末尾的1在哪里,

负数的补码是对正数的原码对它每一位按位取反之后加1得到的,比如:

二进制1100  12
补码 0011 + 1 = 0100
1100 和 0100 与一下得到 0100

那么最终得到的0100 就是2²,我们也就得到了这个数(12)的二进制末尾的1是多少了(详情请学习计算机组成原理)

那么接下来(12)1100再减去0100得到1000 就是这个树状数组下一次要查询的那个节点。这个过程比较巧妙,当然上述给出的代码比较好记,同样lowbit的操作还有如下写法

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

其实也是和补码的思想很相近的,就不赘述了。

假设树状数组用来维护序列a的前缀和,那么

即a[i]之前长为lowbit(i)的区间的数字之和。那么,前缀和 s[i] = s[i−lowbit(i)] + C[i] ,继续递归下去可得:

                      s[i] = C[i] + C[i−lowbit(i)] + C[i−lowbit(i)−lowbit(i−lowbit(i))]..........

思考:一共会被划分多少个C的和?

i - lowbit(i) 相比于i来说,二进制上少了一1,所以其实减lowbit就是在减二进制当中的1,i 的二进制表示中有O(log2)个1,所以s[i]会被划分为O(log2)个C的和,这样求解前缀和s的效率就是O(log2)。

虽然树状数组查询s的复杂度确实不如前缀和的O(1),但它的有点就在于,对序列a中的元素进行修改后,对C数组更新的效率也提高了许多,这正是前缀和无法完成的。

并且,我们给 ii 和 i+lowbit(i)i+lowbit(i) 之间连了一条虚线,从图中我们可以看出,当我们对 a[x]a[x] 进行修改时,C[x],C[x+lowbit(x)],C[x+lowbit(x)+lowbit(x+lowbit(x))],...

也会被修改,因此修改的数量也是 O(log(n)) 个。

 

(图片来自百度百科~)

查询a序列前缀和的代码和修改a序列某个数的代码👇

long long ask(int x) {
    long long ret = 0;
    for (int i = x; i; i -= lowbit(i))
        ret += C[i];
    return ret;
}

void add(int x, int d) {
    for (int i = x; i <= n; i += lowbit(i))
        C[i] += d;
}

不难分析出 add 操作的复杂度同 ask 操作一样,都是 O(log2n) 。

因此,在给出的题目中,如果我们要查询 aa 序列中一段区间 [l,r] 的和,只要输出 ask(r)−ask(l−1) 即可。

区间修改单点查询

问题描述:

给出长度为 n 的序列 a , m 次操作,操作有两种:

  • 给区间 [l,r] 中的数加上 x

  • 询问 a[x] 的值

其实吧这里我们可以用差分来考虑考虑把本体转化为单点修改,区间查询问题,从而使用树状数组。

具体来说,我们用树状数组维护 a 的差分数组 b , b[i]=a[i]−a[i−1]

根据差分的知识,我们知道,给 a[l],a[l+1],...,a[r] 加上 x ,相当于给 b[l] += x ,b[r+1] −= x ;而询问 a[x] 的值,则等价于询问b[j]和的值

因此,只要用树状数组对差分数组 b 做单点修改、区间查询操作就好了。

区间修改区间查询

事实上前两种问题本质上是相同的,可以相互转化。而区间修改区间查询就高级多了。

问题描述:

给出长度为 n 的序列 a , m 次操作,操作有两种类型:

  • 给序列 a 区间 [l,r] 中的数字加上 dd

  • 询问序列 a 区间 [l,r] 中的数字和

我们从询问操作入手来进行分析,再反过来决定修改操作的实现方式。

本题要询问 al + al+1 +... + ar−1 + ar的值,即:

                                                                                                                          

们记 a 的差分数组为 b , b[i]=a[i]−a[i−1] ,那么有:

                                                                                                                              

右侧式子中, b1 被加了 i 次、 b2 被加了i−1 次、...、 bp 被加了 i−p+1 次,于是有:

                                                                    

所以我们可以用树状数组去维护两个前缀和,一个是t1[i]=b[i] 的前缀和,另一个是t2[i]=b[i]×i 的前缀和。

 

 

结语

不太好理解,但也得啃下来!祝大家顺利~

以下是我当年写过的完整的树状数组代码,你看看是那种类型?

#include<bits/stdc++.h>
using namespace std;
#define N 200020
long long a[N], c[N];
long long n, m;
int lowbit(int x){
	return x & (-x);
}
void update(int x, int k){
	for(int i = x; i <= n; i += lowbit(i)){
		c[i] += k;
	}
}
long long getsum(int x){
	long long ans = 0;
	for(int i = x; i; i -= lowbit(i)){
		ans += c[i];
	}
	return ans;
}
int main(){
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		cin >> a[i];
		update(i, a[i] - a[i - 1]);
	}
	for(int i = 1; i <= m; i++){
		long long cnt, x, y, z;
		cin >> cnt;
		if(cnt == 1){
			cin >> x >> y >> z;
			update(x, z);
			update(y + 1, -z);
		}
		else{
			cin >> x >> y;
			long long ans = 0;
			for(int j = x; j <= y; j++){
				ans += getsum(j);
			}
			cout << ans << endl;
			}
	}
	return 0;
}

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值