2019.9.25 初级数据结构——树状数组

一、树状数组基础

学OI的同学都知道,位运算(对二进制的运算)比普通运算快很多。同时我们接触到了状态压缩的思想(即将0-1的很多个状态压缩成十进制的一个数,每一个二进制位表示一个状态)。由于在实际做题过程当中,由于数据范围限制,我们必须采用更高效的存储、查询方法,于是树状数组应运而生。

首先我们检查传统的存储状态。对于数组的每一个下标i,其所存储的有效信息只有一个a[i](这是传统数组)。而对于树状数组,我们每一位下标可以存储lowbit(i)个有效信息。这个lowbit运算一会再说。所以尽管树状数组很难压缩真正的存储空间,但查询的时候可以把n的复杂度压缩成log(n)。这里的查询指区间查询和单点修改。

为什么有这样的查询效率?对于一个区间[1,x],我们把它分成log(x)个小区间。设x=2i1+2i2+……2im(这不由得让我们想起快速幂的分解方法),不妨设i1>i2>……>im,则我们可以把[1,x]分成以下小区间:

(1)长度为2i1的小区间[1,2i1]

(2)长度为2i2的小区间[2i1+1,2i1+2i2]

……

(log(n))长度为2im的小区间[2im-1+1,……+2im]

比如[1,6]可以分成[1,4]和[5,6]

所以我们如果想修改这个区间[1,x]的和,我们只需要修改这log(n)个区间的的和即可。

先上个图:

所以我们现在的问题在于,如果我们要查找a[idx]的前缀和,假设最开始idx是6:

则我们先加上最后一段小区间,令idx=6-21

再加上第一段小区间,令idx=6-21-22

应当注意的是,我们每次多减去的那个2i,实际上那个i是idx的二进制最靠近个位的那一位1对应到的数的编号。

举个例子:

10的二进制是1010,最靠近个位的一个1是在第二位,所以我们第一次减掉22

得到的新数是8,二进制是1000,最靠近个位的一个1是在第四位,所以我们减掉24得到0。

我们减掉2k这个过程,我们把它量化,记原来的数组是a,我们开的数组是c,则每次我们加上的区间就是c[idx],然后idx往前移动到c[idx]存储的区间的前一位,继续这个步骤。

比如说,我们要计算a[1]到a[6]的和,我们让idx=6,我们发现刚才我们拆分[1,6]的结果最后一个小区间是[5,6],所以我们用c[6]记录a[5]+a[6],然后跳到c[4],用c[4]记录a[1]+a[2]+a[3]+a[4],即可得到答案。c数组就是我们说的树状数组。

那到底这个[1,n]的小区间怎么拆分?因为我们每次减掉的那个2i的i都是离个位最近的一个1所在的位置,所以我们只需要按照上述规则从后往前遍历n的所有二进制位,就可以遍历存储这个数的所有小区间。

比如说我们从6(110)遍历到4(100)(其中经过了[5,6]),然后遍历到0,其中经过了[1,4]。

也就是说,我们只需要找到每次要减掉的2i即可。

重点来了!!!

我们定义每次减掉的数是lowbit(i)(其中i是进行减法操作之前的数),也就是说lowbit(i)=2m,其中m是i的二进制最靠个位一个1的位置。定义了这个,我们实际上就是定义c[i]是以a[i]结尾长度为lowbit(i)的a数组内的区间,即c[i]=a[i-lowbit(i)+1]+a[i-lowbit(i)+2]+……+a[i]。这样每次idx跳跃一个lowbit(idx),就相当于恰好跳过了需要被加上的一段小区间,而加上这一段小区间的和只需要O(1)的复杂度

怎么算这个lowbit(i)?这就要用到科学家们的智慧。

lowbit(i)=x&(-x)。

为什么这个数就能满足我们的要求?

我们来补充一点二进制的知识:

我们在存储一个数时,int类型是一个32位有符号整数,其中第一位是符号位,1表示负数,0表示正数。

这里利用的负数的存储特性,负数是以补码存储的,对于整数运算 x&(-x)有

● 当x为0时,即 0 & 0,结果为0;

●当x为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。结果为1。

●当x为偶数,且为2的m次方时,x的二进制表示中只有一位是1(从右往左的第m+1位),其右边有m位0,故x取反加1后,从右到左第有m个0,第m+1位及

其左边全是1。这样,x& (-x) 得到的就是x。

 ●当x为偶数,却不为2的m次方的形式时,可以写作x= y * (2^k)。其中,y的最低位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的二进制表示最

右边有k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第k+1位因为进位的关系变成了1。

左边的位因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为0。结果为2^k。

总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。

一定注意,lowbit(0)会陷入死循环。

所以我们只需要利用这种运算,就可以在log(n)的复杂度内分割小区间,从而计算从a[1]到a[n]的和。

同样我们考虑修改一个值a[idx],因为c数组存储a数组的值有重复,所以我们必须将所有包含a[idx]的c[i]都进行修改。所以,同刚才的步骤,我们只需要根据某种顺序顺次查找每个包含a[idx]的c[i]即可。

这个顺序非常容易想到:

刚才我们已经知道,我们用c[i]表示从i往前数lowbit(i)个a[i]的总和。根据树状数组的特殊结构,我们查找的步骤如下:

找到c[i](肯定包含a[i])----->找到第一个包含c[i]的c[j]--------->找到第一个包含c[j]的c[k]……

一定注意这个步骤,这是因为树状数组的本质是一棵树,一个点只有一个父亲,这个我们之后会说。

根据lowbit的定义,同时根据二进制的特性,有以下两个特征:

(1)lowbit每向左移一位,其表示的lowbit(i)(也就是对应的区间长度,见前文)就乘以2;

(2)根据上文,我们要找到的数实际上是比他大的第一个lowbit往前移动一位的数(比如说5(101),6(110),lowbit移动了一位)。

所以我们怎么找这样的数???

要解决这个问题,我们必须回到二进制的加法:

比如说6+2,在二进制里的表示是这样的:

1 1 0

+0 1 0

---1-------

1 0 0 0

大家很容易注意到,第二位的两个1加起来之后进位到上一位一个1,因为上一位也有一个1,所以必须再进位;以此类推,它会一直进位直到进位到第一个数的lowbit前面的0上,把它变成1。

这难道不是我们刚才要的lowbit移动的方法??我们已经把最后一个1进位到了前面。我们只需要找到第二个和它相加的那个数即可。

因为要进位最后一个1,同时我们需要使底下那个数最小,所以我们只需要找到一个数,它前面后面都是0,中间那个与第一个数最后一个1对上的那一位是1.不明白的参见上面的例子。

有没有觉得上面这个很熟悉????



综上所述,我们得到:

(1)树状数组求前缀和:求1-i的和,每次加上c[i],然后减去lowbit(i)。

(2)树状数组单点修改:修改a[i]时修改c[idx],然后idx+=lowbit(idx),最开始idx=i。

二、树状数组的几何认识

刚才我们已经了解了树状数组的基本操作,下面我们从树本身的角度认识树状数组的性质和操作。

根据上面的图我们可以知道,树状数组具有以下几个性质:

(1)红色节点和其父亲之间的横向距离是lowbit(i),这是树状数组构建的基础,所以我们每次修改一个a[i]的值其实是修改所有以c[i]为根的且包含a[i]的子树的c[i]的值,也就是说从a[i]每次向上找到其父亲,并修改a[i]每一级父亲的值即可。

(2)查找一个点的前缀和时,相当于从这个点每次找到它的儿子覆盖的区间,而每个点覆盖的区间是从这个点到它最左边的儿子,所以每次减去lowbit(i)即可。

高级用法待填坑。

原文地址:https://www.cnblogs.com/qxds/p/11586996.html

时间: 2024-10-02 20:46:00

2019.9.25 初级数据结构——树状数组的相关文章

数据结构——树状数组

我们今天来讲一个应用比较广泛的数据结构——树状数组 它可以在O(nlogn)的复杂度下进行单点修改区间查询,下面我会分成三个模块对树状数组进行详细的解说,分别是树状数组基本操作.树状数组区间修改单点查询的实现.树状数组查询最值的实现 一. 树状数组一般分为三种操作,初始化.修改.查询 在讲基本操作之前,我们先来看一张图 这张图就是树状数组的存储方式,对于没有接触过树状数组的人来说看懂上面这张图可能有些困难,上图的A数组就是我们的原数组,C数组则是我们需要维护的数组,这样存储能干什么呢,比如我们在

HDU 1556 数据结构-树状数组-改段求点

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1556 解题思路:树状数组,只要了解树状数组的原理就不用死记模板了,总之树状数组管理的就是前缀和,高度越高的的结点管理的范围越广 所以要是改点求段:更改一个点就要向上传递 所以要是改段求点:更改一个点就要向下传递 代码: #include <cstdio> #include <iostream> #include <cstring> using namespace std;

query 2019徐州网络赛(树状数组)

query \[ Time Limit: 2000 ms \quad Memory Limit: 262144 kB \] 题意 补题才发现比赛的时候读了一个假题意.... 给出长度为 \(n\) 的排列,在给出 \(m\) 次询问,每次询问有一对 \(l.r\),问满足 \(min(ai, aj) = gcd(ai, aj)\) 的 \(pair(i, j)\) 对数. 思路 考虑离线做 先把每个数出现的位置记录下来,然后预处理出所有的 \(pair\). 对于一个数字 \(x\),其满足条件

用树状数组处理逆序对[数据结构][树状数组]

逆序对 ——!x^n+y^n=z^n 可以到这里[luogu]: https://www.luogu.org/problem/show?pid=1908 题意:对于给定的一段正整数序列,逆序对就是序列中ai>aj且i<j的有序对.知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目. 假如为这些数为: 8 2 3 1 7 如果我们把数一个个加进来,用一个数组a[i]统计i出现了几次. a的初始状态: 8加进来后: 由于不存在比8大的数,说明没有产生逆序对 2加进来后: 统计比2大

数据结构——树状数组篇

原地址:My CSDN Blog  那边还未通过审核,先看这边的吧…… 线段树是一个很好的维护区间关系的这样的一个数据结构,但是,很多时候我们可以用更小空间.更快速度(更大尺寸呢.,全景天窗,五菱宏光?)的数据结构来维护一个前缀关系. ??? 上面的这张图是表示的一个有8个叶子节点的线段树,接下去,我们给它进行一个变形: ??? 然后呢,我们把2.4.6.8.……这样的元素推到最顶端的空上边去,想让2表示1-2这段区间,让4表示1-4这段区间,让6表示5-6这段区间,让8表示1-8这段区间. 然

C++-hdu1166-敌兵布阵[数据结构][树状数组]

单点修改+区间查询=树状数组 空间复杂度O(n) 时间复杂度O(mlogn) 1 #include <set> 2 #include <map> 3 #include <cmath> 4 #include <queue> 5 #include <vector> 6 #include <cstdio> 7 #include <cstdlib> 8 #include <cstring> 9 #include <

C++-hdu1394-Minimum Inversion Number[数据结构][树状数组]

给出0~n-1的一个排列,可以整体移动,求逆序对最小值 把数字num[i]的加入,等价于树状数组的第n-num[i]位加1 因为num[i]是第 (n-1)-num[i]+1=n-num[i]大的数字,产生逆序对,只可能在其之前已经插入了数字,此时直接区间查询即可 1 #include <set> 2 #include <map> 3 #include <cmath> 4 #include <queue> 5 #include <vector>

实用数据结构---树状数组(二叉索引树)

树状数组适用于动态连续和查询问题,就是给定一个区间, 查询某一段的和或者修改某一位置的值. 关于树状数组的结构请去百度百科,否则将看不懂下面内容 我们看这个题 士兵杀敌(二) 时间限制:1000 ms  |  内存限制:65535 KB 难度:5 描述 南将军手下有N个士兵,分别编号1到N,这些士兵的杀敌数都是已知的. 小工是南将军手下的军师,南将军经常想知道第m号到第n号士兵的总杀敌数,请你帮助小工来回答南将军吧. 南将军的某次询问之后士兵i可能又杀敌q人,之后南将军再询问的时候,需要考虑到新

ACM数据结构-树状数组

模板: int n; int tree[LEN]; int lowbit(int x){ return x&-x; } void update(int i,int d){//index,delta while(i<=n){ tree[i]+=d; i+=lowbit(i); } } int getsum(int i){ int ans=0; while(i>0){ ans+=tree[i]; i-=lowbit(i); } return ans; } 示意图: 1.Ultra-Quic