引言
树状数组是一种高级数据结构,又被称为二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为Fenwick树。
树状数组的代码非常简洁,形式很美,只有短短十几行,完爆线段树等其他高级数据结构。同时有非常优秀的时间复杂度、空间复杂度,而且时间常数代价也很小,甚至可以在1000ms内处理超过1e7范围的数据。经过优化,它可以以O(logn)的时间复杂度完成大部分区间修改和区间查询操作,空间复杂度 O(n)。
但树状数组的构造原理也非常难以理解,仅仅背过代码显然是不够的,我们应当深刻理解树状数组的原理。
树状数组引入
lowbit(x)
lowbit(x)表示x的二进制中最后一位1所表示的大小:
树状数组构造原理
用c数组表示树状数组,a数组表示原数组,树状数组的构造原理为:用c[i]维护从a[i]开始往前长度为lowbit(i)的a数组信息。 通常维护的是区间和(sum)。
形式化地说:c[i]维护从a[i]~a[i-lowbit(i)+1]的信息。
如图,树状数组其实是一棵二叉树变形而来:
现在变形一下:
现在定义每一列的顶端节点C数组(其实C数组就是树状数组),如图:
也就是说,c[i]其实挂着两颗子树,但是因为通过存储根的信息和其中一颗子树的信息,可以计算出另一颗子树的信息,因此有一颗子树被省略了。为了方便计算,统一省略位置靠后的那一颗子树,并且把那一颗子树的儿子直接挂到那一颗子树的父亲上。
树状数组的作用
树状数组通常用于进行快速求数组的前缀和,单点修改的功能。
通过优化,可以使得树状数组得以实现区间修改、区间查询的操作。
注意,这里的“求和”是广义上的求和,并不单指数学加法,只要具备“可减性”即可。(即对于一个运算来说,存在逆运算)
用树状数组还可以维护max,min等信息,但这一类信息不具备可减性,因此只能实现部分功能。
树状数组的性质
首先有几个定义和公式:
下面给出证明:
1.前面已经说过,c[i]管理的长度是向前lowbit(i),因此在这个范围内的c数组全部是c[i]的子树,因此,c[i-lowbit(i)]表示从c[i]向前跳它的长度,这样就达到了直接前驱。
2.根据树状数组的构造原理,在一个结点上原本挂着两颗大小相同的子树,这里的大小相同蕴含着维护的区间长度相同,因此,c[i+lowbit(i)]表示跳到与c[i]同一深度的子树的根结点,也就是它的直接后继(因为c[i]同一深度的子树的根结点被省略了)。
构造树状数组
下面给出代码:
int lb(int x) {
return x&-x;
}
lb函数,即lowbit函数,通常在int前加上inline关键字表示内联建议,以提高运行速度。(但其实无伤大雅)
关于lowbit函数的原理,请查阅原码、反码和补码,但这不是重点。
向树状数组中加入一个元素,需要修改c[i]和c[i]的所有后继:
void push(int x,int v) {
while(x<=n) c[x]+=v,x+=lb(x);
}
求前缀和,需要累加c[i]和c[i]的所有前驱:
int find_sum(int x) {
int sum=0;
while(x) sum+=c[x],x-=lb(x);
return sum;
}
如果需要初始化树状数组,可以看做树状数组里的元素原来全都为0,直接push即可。
如果需要求区间[x,y]的和,那么利用差分,只需要求find(y)-find(x-1)。
如果需要修改区间[x,y]的值,那么用树状数组维护查分数组。
如果需要实现区间修改和区间查询操作,需要另外维护一个树状数组…
诸如此类的基础操作不在细谈。
其他细节
树状数组的下标
树状数组的下标必须从1开始,这是由循环条件while(x)决定的,lowbit(0)无意义。
当然在实现上,如果写特判,树状数组从0开始存也不是办不到,但这纯属吃饱了撑的。
时间复杂度分析
显然,x-=lowbit(x)和x+=lowbit(x)这些操作最多执行log2(n)轮,因为n最多有log2(n)位二进制表示,最坏情况下n的所有位上全部填1。
树状数组与线段树
所有树状数组能实现的功能用线段树都可以实现,而且有些树状数组无法实现的功能线段树也可以实现。但线段树代码确实不如树状数组好写,而且线段树常数略大,1e7应该是莽不过去了。
高维树状数组
高维树状数组比一般的树状数组更加抽象,但万变不离其宗,其实也很简单,我们这里用二维树状数组来举例:
用c[i][j]来表示从a[i][j]开始,向i的前方走lowbit(i),向j的前方走lowbit(j)范围的区间和。假设现在一个二维树状数组一样的树生长在一个网格上,用一个垂直于网格的平面去切树状数组(沿着网格的格点),得到的树状数组也是一颗被省略了结点的二叉树。所以上述公式仍然成立。
代码上也很好理解:
以此类推。(其实我很想画图,但是画工不允许)
可持久化树状数组
一般没有可持久化树状数组的写法,毕竟树状数组的优势在于代码简洁和常数小,一旦加上可持久化那么树状数组的优势就不复存在了,因此一般直接使用可持久化线段树等数据结构。
参考文献
树状数组最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE上。
陈小玉 《算法训练营》进阶篇配套视频 树状数组,就是这么简单!
ToLoveToFeel 树状数组 版权声明:本文为CSDN博主「ToLoveToFeel」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
ToLoveToFeel 计算机中数的表示 版权声明:本文为CSDN博主「ToLoveToFeel」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。