树状数组系列题目总结
M n 2i n lg n (前面两种均为Θ(n) ) ,而且在实际编程中,线段树通常是使
i 0
lg n
用指针进行实现的, 这样的递归和调用就为我们的时间复杂度填上了一个不小的常数 (虽然 在实际分析中常常省去常数的大小,但是在此处它的确是一个不可忽略的因素) 。更要命的 是,线段树的编程难度比较大,稍一疏忽就容易写错,也会消耗很多编程的时间。 在分析了几种不是很乐观的方案之后,沮丧之余,树状数组的出现就像救世主一样拯 救了我们的思路。它可以在Θ(lg n) 的时间内完成一次查询或是修改操作,且只占用Θ(n) 大小的空间(也就是一个数组) ,而且它的代码十分短小精悍,只要牢记它的基本思想就可 以在很短的时间内写出准确无误的代码。
Input
The first line of the input file contains a number of stars N (1<=N<=15000). The following N lines describe coordinates of stars (two integers X and Y per line separated by a space, 0<=X,Y<=32000). There can be only one star at one point of the plane. Stars are listed in ascending order of Y coordinate. Stars with equal Y coordinates are listed in ascending order of X coordinate.
-1-
树状数组系列题目总结 南开中学 莫凡
S16
S8
S4 S2 S1 S3 S5 S6 S7 S9 S10 S11
S12 S14 S13 S15
A1
A2
A3
A4
A5
A6
A7
A8
A9
A10 A11
A12
A13
A14
A15
A16
关于这个图应该多说几句,否则不是很好理解。我们用 A 来表示原来给定的数组,S 表示我们构造出的树状数组。图中两点之间的连线表示一种相加关系,根据这张图,我们是 这样定义这个 S 数组的:S[n]的值等于所有在 S 的下方且与 S 有连线的点的值的和。例如, S[1]=A[1], S[2]=S[1]+A[2], 以及 S[12]=S[10]+S[11]+A[12]等等。 字面上 S 数组的准确定义并 不是那么容易描述, 但是这个图为我们呈现了一种显而易见却又难以描述的规律 (使自己明 白这种规律的最好方法就是照猫画虎重新画一个 32 节点的树) 。 我们现在要做的是仔细研究 分析出它的规律和定义方式,然后对它的效率进行简单的分析。 一步登天总是妄想,对一件事情着手分析时,应该尽可能从最简单的情况入手(但有 时候情况过于简单,什么结论都无法得到) 。假设现在,我们需要将 A[6]到 A[14]的和用 S 数组来表示,我们可以得到这样的结果:sum = S[14] + S[12] + S[8] - S[4] - S[5]。这时候可 以回过头去看一眼上一节所说的方案二, sum = B[14] – B[5]。 很显然的, B[14] = S[14] + S[12] + S[8], B[5] = S[5] + S[4]。在查询的时候我们依旧采用方案二中的思路,就将这个问题转化 成了使用 S 数组来表示 B 数组的问题了,由于符号单一而且更容易发现规律,我们很快可 以得出这件事(接下来的伪代码中我们尝试使用 S 数组来表示 B[n]): ans←0; while(n > 0) begin ans←ans + S[n]; if(n 是 2^x 的倍数而不是 2^(x + 1)的倍数) //x 是一个唯一符合要求的自然数 then n←n-2^x; end; 现在问题的关键转变为了如何去计算这个 x,或是 2^x,潜意识中 2^x 总是和 x 位二进 制有关系——2^x 在二进制中是 1 后面加上 x 个 0。 我们需要了解一个显而易见的引理: 引理 1 一个数被 2^x 整除的充要条件是它的二进制形式的后 x 位均为 0 由于 n 是 2^x 的倍数,所以 n 的后 x 位一定都是 0,而且 n 不是 2^(x+1)的倍数,所以 n 的倒数第 x 位一定为 1,也就是说,我们只需要找出 x 的二进制中最后一个是 1 的位连同它 之后的 0 构成一个新的二进制值, 我们将其记做 lowbit(n) (我们将在下一节中讨论它的计算 方法) 。这样一来,计算 B[n]的详细代码就可以写出(code in C++):
Sample Input
5 1 1 5 1
-4-
树状数组系列题目总结 南开中学 莫凡
7 1 3 3 5 5
Sample Output
1 2 1 1 0
B 题意分析
题目中给出一些(n 个)星星的坐标,将每颗星星的等级定义为纵坐标小于等于它且横 坐标小于等于它(不包括自己)的星星数,而且不会出现两颗星星坐标相同的情况,要求统 计出每一等级(0 ~ n-1)的星星总数。特别地,输入数据按照纵坐标的升序排序,在纵坐标 相等时按照横坐标的升序排序给出
-2-
树状数组系列题目总结 南开中学 莫凡
int B(int n) { int ans = 0; while(n > 0){ ans += S[n]; n -= lowbit(n); } return ans; } 当然,像下面这样写会更简洁: ans = 0; for(int i = n; i > 0; i -= lowbit(i)) ans += S[i]; 通过延续方法二的思路使查询变得简单起来,通过使用 S 数组现场计算 B 数组可以缩 短修改 B 数组的时间。修改的操作和查询几乎如出一辙,通过观察,只需要进行以下操作 即可完成修改: void change(int x, int V) //x 是要修改的位置,V 是改变量 { for(int i = 1; i <= Array_size; i +=lowbit(i)) S[i] += V }
树状数组系列题目总结 南开中学 莫凡
树状数组系列题目总结
第一部分 树状数组数据结构浅析
一、我们面对的问题
假设有这样一个问题:现在我们有一个给定的初始数列 A,它支持两种操作:修改和查 询。修改操作是这样的:在第 a 个数上加上 b(当然,也有可能是减去) 。查询操作要求输 出第 i 个数到第 j 个数之间的总和。 如果直接去处理一个数组的话就可以在Θ(1)的时间内完成一次修改操作,但是它需 要话费Ο(n)的时间完成一次查询操作,这样的效率非常的低,在查询操作比较多而且查 询的区间比较长的话会花费极大的时间。 在统计一段数字的和的时候我们会想到使用一个数组 B[n]来存储 1~n 的部分和,然后 查询时只需要输出 B[j] – B[i – 1]的值即可,这样的优化就将查询的时间减小到了Θ(1) ,但 是很不幸,在修改第 i 个数字的值的时候,B[i]及之后的每一个部分和都需要被修改,这样 就使得每一次修改的时间变为了Ο(n) ,仍然不是高效的方法。 到这时我们不由得想到处理连续区间上问题的必杀神器线段树了,当然,线段树可以 有效地平衡查找和修改的时间消耗,使得问题得以解决,但是线段树的空间复杂度比较大
For example, look at the map shown on the figure above. Level of the star number 5 is equal to 3 (it's formed by three stars with a numbers 1, 2 and 4). And the levels of the stars numbered by 2 and 4 are 1. At this map there are only one star of the level 0, two stars of the level 1, one star of the level 2, and one star of the level 3. You are to write a program that will count the amounts of the stars of each level on a given map.
C 算法分析
这题属于树状数组的基础题。 根据观察题目中给出的输入数据的性质就可以发现, 在每 颗星星的坐标被输入时,它的等级已经确定(所有满足条件的星星都已经在它之前给出) 。 而且它保证了之前的每一颗星星都满足纵坐标的条件, 所以我们只需要统计当前横坐标小于 等于 Xi 的星星个数就可以计算出 Stari 的等级了,所以通过使用一个树状数组统计每个横坐 标位置上有多少颗星星即可了。 需要注意的是为了使树状数组的下标从一开始, 我们在读入 之后应该将横坐标+1 之后在进行统计。
Output
The output should contain N lines, one number per line. The first line contains amount of stars of the level 0, the second does amount of stars of the level 1 and so on, the last line contains amount of stars of the level N-1.
三、巧妙的 lowbit 运算
lowbit 可谓是树状数组这个数据结构的精华思想所在,它决定了树状数组的定义、修改 和查询。可想而知,lowbit 的运算应该是在常数时间内完成的,否则树状数组的时间效率绝 不可能如此之高。很显然,既然是取最后一位,工作自然是通过位运算完成的。在此,我们 不再去探究它的探索过程,而是直接给出结论在对其进行说明。 lowbit(x) = x and (-x) 由于计算机中使用补码的存储方式,-x 的值实际上是 x 按位取反再+1。首先抛开位数 的限制我们要明白一个问题,那就是 10000……(x 个 0)的取反是 01111……(x 个 1) ,再 加一结果依然是 10000……(x 个 0)而无论倒数第一个 1 的左侧是什么,在取反之后都不 会受到+1 的影响,也就是说,x 与-x 在补码的存储方式中有且仅有倒数第 x+1(x 为右侧 0 的个数)位同为 1,用它们进行按位与运算即可得出 lowbit 的正确结果。