关于树状数组的个人理解

本篇只对树状数组的理论部分进行讲解,其应用在之后的博客中再进行专题讲解
以下内容部分转载于:
彻底理解树状数组
树状数组彻底入门

  • 树状数组(Binary Indexed Tree)——二进制索引树
  • 树状数组问题模型
    首先我们搞明白树状数组是用来干嘛的,现在有一个这样的问题:有一个数组a,下标从0到n-1,现在给你w次修改,q次查询,修改的话是修改数组中某一个元素的值;查询的话是查询数组中任意一个区间的和,w + q < 500000。

这个问题很常见,首先分析下朴素做法的时间复杂度,修改是O(1)的时间复杂度,而查询的话是O(n)的复杂度,总体时间复杂度为O(qn);可能你会想到前缀和来优化这个查询,我们也来分析下,查询的话是O(1)的复杂度,而修改的时候修改一个点,那么在之后的所有前缀和都要更新,所以修改的时间复杂度是O(n),总体时间复杂度还是O(qn)。

可以发现,两种做法中,要么查询是O(1),修改是O(n);要么修改是O(1),查询是O(n)。那么就有没有一种做法可以综合一下这两种朴素做法,然后整体时间复杂度可以降一个数量级呢?有的,对,就是树状数组,以空间换时间。

  • 一维树状数组

在这里插入图片描述

  • 前缀和问题的解法:二进制分解
    当查询区间[1,15]时,其示意图如下图所示:
    在这里插入图片描述

操作时,我们依此跳过上面的2的3次方、2次方、1次方、0次方 这4段,跳出的距离分别是8,4,2,1
也就是说15=2"3+2"2+2"1+2"0,这就是15的二进制分解,在二进制中表达为(1111)2。

那么,怎样处理这种跳跃关系?

  • 从后向前跳跃——lowbit技术
    考虑一个数129,它的二进制表示为(10000001)2,129=2"8+2"0,但是还是要枚举1…7的部分,然而因为系数是0,所以它们对答案并没有贡献(类似于计算0×2"7),在if(x&1)就会被pass掉。
    这里介绍一种常数优化:lowbit(x),它可以O(1)的直接调到我们需要的位置,在最坏的情况下,这样做的总复杂度依然是logn的。
int lowbit(int x)
{//返回二进制数最低位的1对应的数值
	return x & (-x);      //与运算
}
  • 用这种二进制跳跃关系处理原数组A【】,树状数组C【】,前缀和数组S【】之间的关系

原数组A【】,树状数组C【】,前缀和数组S【】之间的关系
最左侧为对应二进制,树状数组C【】存在的目的是存储从后向前的最近一次跳跃,可用lowbit技术进行计算,跳跃的元素个数为lowbit,
作用范围为:
C[i]=A[i-2"k+1]+A[i-2"k+2]+…A[i];

C[i]=A[i-lowbit(i)+1]+A[i-lowbit(i)+2]+…A[i];
求前缀和的时候便可用树状数组C【】来处理二进制分解对应的跳跃:
如上图的S【7】=C【7】+C【6】+C【4】(每次减一个lowbit)

int sum_pre(int val[],int i)
{//求arry数组的前i项和
 //val为树状数组地址
	int sum = 0;
	for (;i > 0;i -= lowbit(i))       //从后向前每次跳一个lowbit
		sum += val[i];
	return sum;
}

int sum_between(int val[],int pre, int last)
{//求原数组arry在区间[pre-last]的和
	return sum_pre(val, last) - sum_pre(val, pre - 1);
}
  • 对更新问题的处理
    当A【i】的值改变时,如何对对应的树状数组进行更新
    其实从第一张图我们可以看出需要对C【i】的父结点进行更新,那么如何获取这个值呢?
    更新和查询其实可以近似看成是“逆过程”,这个只是理解,如何实际操作这个问题呢?
    i=i+lowbit(i)
    为什么是加上lowbit(i)呢?
    不失一般性,我们不妨假设分析:
    设i=i+range
    1.range>lowbit(i):
    因为每个C【i】对应一个跳跃范围(作用范围),此时i=i+range后,这个lowbit(i)仍为之前的lowbit(i)(因为最低位1的位置没有变化),所以,range>作用范围,之前的A【i】不在范围内,所以此情况不成立;
    2.range<lowbit(i):
    此时i=i+range后,对应的lowbit(i)变小,此时C【i】的作用范围变小,范围为lowbit(range),
    当lowbit(range)<range时:按照第一种情况得出的结论,不成立;
    当lowbit(range)=range时:此时作用范围为C[i]=A[i-lowbit(i)+1]+A[i-lowbit(i)+2]+…A[i],恰好未包含之前的A【i】,所以,不成立。
    综上所述,i=i+lowbit(i);
void update(int val[], int i, int cal,int arry_num)
{//原数组第i个元素加上cal,更新树状数组相关元素,arry_num为原数组的长度
 //可直接用于树状数组的建立
	for(;i<=arry_num;i+=lowbit(i))
		val[i]+=cal;
}

对于更新函数update用于初始化树状数组val的用法如下:

memset(val,0,sizeof(val));    //先将val数组初始化为0
for(int i=1;i<=arry_num;i++)
update(val,i,arry[i],arry_num);

以上便是我对一维树状数组的理解。

  • 二维树状数组
    一维树状数组是用于子区间求和,那么二维树状数组便是用于子矩阵求和

    扫描二维码关注公众号,回复: 3579035 查看本文章
  • 相关函数操作
    二维树状数组的基本原理仍为lowbit技术
    我们先来介绍一下子矩阵求和

  • 子矩阵求和
    只是比一维树状数组多了一个for循环,因为A【i】【j】的子矩阵之和=∑∑A【i】【j】,需要两次for循环解决

int sum_pre_2(int val[][MAXSIZE], int x, int y)
{//求解A[x][y]左上方的子矩阵A[1--x][1--y]
	int sum = 0;
	for (int i = x;i > 0;i -= lowbit(i))
		for (int j = y;j > 0;j -= lowbit(j))
			sum += val[i][j];
	return sum;
}

  • 更新
    更新操作其实是求个操作的逆过程,但本质上都是找与A【i】【j】相关的C【i】【j】,只不过求和是向下找,更新是向上找
void update_2(int val[][MAXSIZE], int x, int y, int cal, int arry_num_x,int arry_num_y)
{//当原数组A[x][y]+cal时,更新树状数组val,arry_num为行和列的最大长度
	for (int i = x;i <= arry_num_x;i += lowbit(i))
		for (int j = y;j <= arry_num_y;j += lowbit(j))
			val[i][j] += cal;
}
  • 更高维的树状数组
    树状数组的优点之一便是很容易扩展到高维
    对于更高维的树状数组只需在原有操作函数的基础上增加for循环次数即可

猜你喜欢

转载自blog.csdn.net/qq_40432881/article/details/83034539