从前缀和到树状数组

树状数组

一、前缀和数组回顾

S i = a 1 + a 2 + . . . + a i S_i = a_1 + a_2 + ... + a_i Si=a1+a2+...+ai

a i a_i ai :原数组

差分数组: X i = a i − a i − 1 X_i = a_i - a_{i - 1} Xi=aiai1

关系:原数组是前缀和数组的差分数组;原数组是差分数组的前缀和数组。

前缀和数组与差分数组并没有增加信息,只是原数组信息的另外一种表示,在处理问题时数据的处理时间复杂度不同。

问题1:求原数组的区间和,原数组上的时间复杂度为 O ( n ) O(n) O(n),前缀和数组上操作时间复杂度为 O ( 1 ) O(1) O(1).

问题2:原数组区间元素修改(同时加一个值)

例如a2到a5的元素都增加d, 体现在差分数组上为x2+d, x6 - d, 其他元素不变。即只有两个点发生变化。

上面两类问题下原数组时间复杂度为 O ( n ) O(n) O(n), 差分数组上的时间复杂度为 O ( 1 ) O(1) O(1).

二、lowbit函数与树状数组

lowbit(i) 表示 对于 i 这个数,二进制表示的最后一位1的位权

Lowbit(8) = (1000) = 8, lowbit(6) = (110) = 2, lowbit(12) = (1100) = 4 …

公式:
l o w b i t ( a ) = a & ( − a ) lowbit(a) = a \& (-a) lowbit(a)=a&(a)

上述公式的简单证明:

回顾计算机中的负数表示。

负数 = 补码 = 反码 + 1 负数=补码=反码+1 负数=补码=反码+1

假设计算机一共有四位,那么1的二进制表示为0001, -1的二进制表示:

− 1 = ( 0001 ) 补 = ( 0001 ) 反 + 1 = 1110 + 1 = 1111 -1 = (0001)_补 = (0001)_反 + 1 = 1110 + 1 = 1111 1=(0001)=(0001)+1=1110+1=1111

补码的巧妙之处在于,任何一个正数的二进制表示,加上它负数的二进制表示,正好是0的二进制表示。例如假如计算机一共有四位,那么:

1 − 1 = 1 + ( − 1 ) = ( 0001 ) + ( 1111 ) = 0000 1 - 1 = 1 + (-1) = (0001) + (1111) = 0000 11=1+(1)=(0001)+(1111)=0000

理解了补码,再来看对于任意一个正数a, a & ( − a ) a \& (-a) a&(a) 是什么:

假设a 的二进制表示为: ( x x x x 10000 ) (xxxx10000) (xxxx10000), 前面的x表示1或0, 最后一个1的位置表示出来。

那么-a的二进制表示为: ( x x x x 10000 ) 反 + 1 = ( x ′ x ′ x ′ x ′ 01111 + 1 ) = ( x ′ x ′ x ′ x ′ 10000 ) (xxxx10000)_反 + 1 = (x'x'x'x'01111 + 1) = (x'x'x'x'10000) (xxxx10000)+1=(xxxx01111+1)=(xxxx10000); (x’ = 1-x).

所以
a & ( − a ) = ( x x x x 10000 ) & ( x ′ x ′ x ′ x ′ 10000 ) = ( 10000 ) a \& (-a) = (xxxx10000) \& (x'x'x'x'10000) = (10000) a&(a)=(xxxx10000)&(xxxx10000)=(10000)

正好表示了最后a的最后一个1对应的二进制数字,即 l o w b i t ( a ) lowbit(a) lowbit(a)

证毕。

理解了lowbit函数后,接下来介绍树状数组。

树状数组的概念:改进前缀和:树状数组C(i) 代表原数组a[i] 前面lowbit(i) 项的和

lowbit(10) = 2, C[10] = a[10] + a[9];
lowbit(12) = 4, C[12] = a[12] + a[11] + a[10] + a[9]

树状数组和前缀和数组相比,对原数组单点修改时,树状数组修改的值更少(前缀和数组可能要全部修改一遍)

能用前缀和数组解决的问题都能用树状数组解决;

三、利用树状数组实现前缀和数组的查询:

由C[i]的定义可知:
C [ i ] = S [ i ] − S [ i − l o w b i t ( i ) ] C[i] = S[i] - S[i - lowbit(i)] C[i]=S[i]S[ilowbit(i)]

所以可以得到,S[i] = S[i - lowbit[i]] + C[i]

用这个公式可以递推的有C[i]得到S[i], 例如:

S[7] = S[6] + C[7] = S[4] + C[6] + C[7] = C[4] + C[6] + C[7]

S[12] = S[8] + C[12] = C[8] + C[12]

所以利用树状数组可以实现前缀和数组的查询,而且效率也较高,为log(n).

四、利用树状数组实现原数组的单点修改

当要原数组修改位置5的原数组值时,要修改C[5]的值,还要修改C[5 + lowbit[5]] = C[6], C[6 + lowbit(6)] = C[8] ,以此类推。。。

为什么能这样操作?为什么i+lowbit(i)就能实现所有数组的修改?可以这样来理解:

lowbit(i) 表示 i 这个数字二进制后面最后一个1.

i + lowbit(i) 表示 i 这个数字最后一个数字产生了进位。

假设 a = i, b = i + lowbit(i). b即表示a的最后一个二进制1产生的进位。

所以 lowbit(b)的值最起码是lowbit(a) 值的2倍。 即C[b]所能覆盖的范围最起码是C[a]所能覆盖范围的2倍。所以b所管控的范围必然包含a所管控的范围。

例如C[5] 上面是C[6], 所以C[6]控制的范围必然包含了C[5], 6上面是8, 所以C[8]必然包含C[6]。

五、 树状数组的代码实现

1. 基本功能实现
#include <vector>
#include <iostream>
#include <string>

using namespace std;

#define lowbit(x) (x & (-x))

class FenwickTree{
	public:
		FenwickTree(int size): n(size), c(size + 1){}
		//size表示最大访问到的下标,所以c数组的长度就是size + 1;

		void add(int i, int x) {
			//单点修改:在原数组的第i个位置上的元素加x
			while(i <= n) {
				c[i] = c[i] + x;
				i = i + lowbit(i);
			}
			return;
		}

		int query(int x) {
			//对原数组的前x位进行前缀和查询
			int sum = 0;
			while (x) {
				sum += c[x];
				x -= lowbit(x);
			}
			return sum;
		}

		void output() {
			int len = 0;
			for (int i = 1; i <= n; i++) {
				len += printf("%5d", i);
			}
			printf("\n");
			for (int i = 1; i <= len + 6; i++) printf("=");
			printf("\n");
			for (int i = 1; i <= n; i++) {
				printf("%5d", c[i]);
			}
			printf("\n");
			for (int i = 1; i <= n; i++) printf("%5d", query(i) - query(i - 1));
			printf("\n");
			return;
		}

	private:
		int n; //树状数组所维护的下标的最大上限
		vector <int> c;  //树状数组



};

int main() {
	int n;
	cin >> n;
	FenwickTree tree(n);
	for (int i = 1, a; i <= n; i++) {
		cin >> a;
		tree.add(i, a);
	}
	tree.output();

	return 0;
}

输入原数组:

10
1 1 1 1 1 1 1 1 1 1

输出:

    1    2    3    4    5    6    7    8    9   10
========================================================
    1    2    1    4    1    2    1    8    1    2
    1    1    1    1    1    1    1    1    1    1

输出的第一行表示1-10的下标,第二行表示树状数组,第三行表示原数组。
所以树状数组仅仅是原数组的另外一种数据表现形式。

2. 将数组中特定位置的数改成另一个数(单点修改)
#include <vector>
#include <iostream>
#include <string>

using namespace std;

#define lowbit(x) (x & (-x))

class FenwickTree{
	public:
		FenwickTree(int size): n(size), c(size + 1){}
		//size表示最大访问到的下标,所以c数组的长度就是size + 1;

		void add(int i, int x) {
			//单点修改:在原数组的第i个位置上的元素加x
			while(i <= n) {
				c[i] = c[i] + x;
				i = i + lowbit(i);
			}
			return;
		}

		int query(int x) {
			//对原数组的前x位进行前缀和查询
			int sum = 0;
			while (x) {
				sum += c[x];
				x -= lowbit(x);
			}
			return sum;
		}
		int at(int ind) {
			//原数组在ind位置的值
			return query(ind) - query(ind - 1);
		}
		void output() {
			int len = 0;
			for (int i = 1; i <= n; i++) {
				len += printf("%5d", i);
			}
			printf("\n");
			for (int i = 1; i <= len + 6; i++) printf("=");
			printf("\n");
			for (int i = 1; i <= n; i++) {
				printf("%5d", c[i]);
			}
			printf("\n");
			for (int i = 1; i <= n; i++) printf("%5d", query(i) - query(i - 1));
			printf("\n\n\n");
			return;
		}

	private:
		int n; //树状数组所维护的下标的最大上限
		vector <int> c;  //树状数组



};

int main() {
	int n;
	cin >> n;
	FenwickTree tree(n);
	for (int i = 1, a; i <= n; i++) {
		cin >> a;
		tree.add(i, a);
	}
	tree.output();

	int ind, val;
	while (cin >> ind >> val) {
		cout << "change " << ind << " to " << val << endl;
		tree.add(ind, val - tree.at(ind));
		tree.output();
	}
	return 0;
}

输入:

10
1 1 1 1 1 1 1 1 1 1
5 10
3 6
2 7
4 9

输出:

    1    2    3    4    5    6    7    8    9   10
========================================================
    1    2    1    4    1    2    1    8    1    2
    1    1    1    1    1    1    1    1    1    1


change 5 to 10
    1    2    3    4    5    6    7    8    9   10
========================================================
    1    2    1    4   10   11    1   17    1    2
    1    1    1    1   10    1    1    1    1    1


change 3 to 6
    1    2    3    4    5    6    7    8    9   10
========================================================
    1    2    6    9   10   11    1   22    1    2
    1    1    6    1   10    1    1    1    1    1


change 2 to 7
    1    2    3    4    5    6    7    8    9   10
========================================================
    1    8    6   15   10   11    1   28    1    2
    1    7    6    1   10    1    1    1    1    1


change 4 to 9
    1    2    3    4    5    6    7    8    9   10
========================================================
    1    8    6   23   10   11    1   36    1    2
    1    7    6    9   10    1    1    1    1    1


如上即实现了树状数组的相关操作。

学习树状数组需要学习里面的细节,也需要有抽象化的能力,那就是树状数组是用来维护前缀和的。应用的时候,只需记住这一点,至于里面实现的细节,可以忘掉。这就是抽象化的能力。

接下来的文章将举例讲述树状数组在Leetcode中的应用。

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值