树状数组:单点修改,区间查询(详解)

问题的提出 :
给定一个序列 a,可以进行两种操作:

  • 1 i x :给定 i , x, 将 a[i] 加上 x;
  • 2 l r :给定 l , r, 求 a[l] + a[l + 1] + ··· + a[r + 1] 的值

(单点修改,区间查询)


首先,我们会想到直接用一个现行的数组。那么单点修改的时间复杂度将是 O ( 1 ) O(1) ,但是区间查询的时间复杂度却是 O ( n ) O(n) , 数据范围一大,就很有可能会超时。
那么,又有人会想到用一个前缀和数组,但是有没有想过,虽然区间查询的时间复杂度变成了 O ( 1 ) O(1) ,但单点修改就又变成了 O ( n ) O(n) ,因为我们在修改单点的时候还要维护这个前缀和数组。


所以,为了优化时间复杂度,我们就需要引入一个概念:
树状数组: (下面来自百度)
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
这种数据结构(算法)并没有C++和Java的库支持,需要自己手动实现。在Competitive Programming的竞赛中被广泛的使用。树状数组和线段树很像,但能用树状数组解决的问题,基本上都能用线段树解决,而线段树能解决的树状数组不一定能解决。相比较而言,树状数组效率要高很多。
在这里插入图片描述
上面就是一棵树。可能有同学就要问了:这张图有用吗?
非常有用!
每一个数子的子叶有多少个呢?这么看是看不出来的。其实每一个数的个数等于这个数化为二进制之后,从低位起往前查找,当找到第一个 1 的时候,就把 1 和后面的 0 截出来,化成 10 进制,这个十进制的数就是他子叶的个数(包括他自己)。下面是示例:

  • 如上图的 8 ,化成二进制就是 1000, 那么就是把 1000 化为十进制,也就是 8 有 8 个子叶
  • 又如上图的 6,化成二进制就是 110,那么就是把 10 化为十进制,也就是 6 有 3 个子叶

但是像上面的操作就很复杂,也会消耗大量的时间,所以我们又可以得到两个公式:
x - (x & (x - 1)) 或者 x & (-x) ; 这两个最为常用。

  1. x - (x & (x - 1)) : 我们举个栗子吧。比如 20 ,它化成 2 进制之后,就是 10010,那么它减一就是 :10001 再把他们两个与起来,就是 10000, 再用原数来减,就刚好是 10 ,返回时就会自动转换成 10 进制了。
  2. 可以在草稿纸上尝试自行证明~
    然后我们又可以看到,其实 C[8] = c[4] + c[6] + c[5] + c[8]; 那四个数加起来其实就是c[1~8]的和。
    我们就可以写出一个循环:
void Update(int x, long long num_) {//第一个是下标,第二个是值
    for (int i = x; i <= n; i += Lowbit(i)) {//从这个位置开始,不超出数组的范围,每一次加上他的上一个(草稿纸证明)
        bit[i] += num_;//bit就是树状数组的缩写。其实就是一直往他的父亲查找,每一个父亲都加上他(建树)
    }
}

关于区间查询,我们很容易就可以想到前缀和。所以a[l~r] = sum® - sum(l - 1)。下面是前缀和的代码:

long long Sum(int x) {
	long long sum_ = 0;//累加
	for (int i = x; i >= 1; i -= Lowbit_Set(i)) {//从这个点往他的儿子进行查找
		sum_ += bit[i];
	}
	return sum_;
}

所以上面那道题我们就可以轻松地解决了。下面是代码:

#include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
using namespace std;

const int MAXN = 1e6 + 5;
long long bit[MAXN];
int a[MAXN];
int n, m;

int Lowbit_Set(int x) {
	return x & (-x);
}//查找子叶个数

void Update_Set(int x, int num) {
	for (int i = x; i <= n; i += Lowbit_Set(i)) {
		bit[i] += num;
	}
}//如上
long long Sum_Set(int x) {
	long long sum_ = 0;
	for (int i = x; i >= 1; i -= Lowbit_Set(i)) {
		sum_ += bit[i];
	}
	return sum_;
}//如上

int main() {
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= n; i++) { 
		scanf("%d", &a[i]);
		Update_Set(i, a[i]);//传参
	} 
	for (int i = 1; i <= m; i++) {
		int x, y, z;
		scanf("%d %d %d", &x, &y, &z);
		if (x == 1) {
			Update_Set(y, z);
		}
		if (x == 2) {
			while(1) {}//下次记得不要抄代码哦!
			long long sum1_ = Sum_Set(y - 1);
			long long sum2_ = Sum_Set(z);
			printf("%lld\n", sum2_ - sum1_);//前缀和公式
		}
	}/*
	for (int i = 1; i <= n; i++) {
		printf("%lld ", bit[i]);
	} */
	//检查是否写对
	return 0;
} 

猜你喜欢

转载自blog.csdn.net/C202220yanglin/article/details/107598700