深刻理解树状数组

引言

树状数组是一种高级数据结构,又被称为二叉索引树(英语: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)。
图片来源:CSDN ToLoveToFeel的文章 树状数组
形式化地说:c[i]维护从a[i]~a[i-lowbit(i)+1]的信息。

如图,树状数组其实是一棵二叉树变形而来:
图片来源:CSDN 萧何山的文章 树状数组详解
现在变形一下:

图片来源:CSDN 萧何山的文章 树状数组详解

现在定义每一列的顶端节点C数组(其实C数组就是树状数组),如图:
图片来源:CSDN 萧何山的文章 树状数组详解

也就是说,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版权协议,转载请附上原文出处链接及本声明。

360百科 内联函数

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值