Fenwick Tree 数据结构可能很难从根本上掌握,但一旦你了解了底层数学,你应该会擅长它。所以我将尝试解释所有关于Fenwick Trees的如何和为什么。
Fenwick Tree 基于数组索引的二进制表示
首先,你应该坚定地理解的是:
Fenwick 树的想法是基于一个事实,即每个整数都可以表示为二进制数,即作为 2 的不同幂的总和,并且该表示将是唯一的;例如整数 14 可以表示为 23+22+21。
请注意,“不同”是此定义中的重要关键字,因此您不应将 14 表示为 23+21+21+21.
芬威克树的种植方式
这里我不会实现Fenwick Tree 填充算法(你说,你明白树是怎么填充的,另外,和问题无关);但是,我要强调一个事实,Fenwick 树 [大部分] 是通过数组实现的,在某种程度上,fenwick-tree 数组中的每个槽都保存一个值,即原始数组的范围之和,其中:
- 该范围内的右索引是 k 本身(这个槽是右边界);
- 该范围内的元素数是该索引的 sum-of-the-powers-of-two 表示中的最小 addend(因此,您应该计算那个元素的数量,从右到左,以便得到有问题的范围)。
P。 S. 如果 Fenwick 树 在索引 24 处存储了一些 n 值,这意味着,原始数组中的区间 [17, 24] 的总和 em>,将是 n。
问:为什么左边是17?
A:因为,24 是 24+23,并且此表达式的最小加数是23 = 8。现在,根据上面给出的定义,在 Fenwick Tree 数组中索引 24 处元素的总和将包含 8 个元素,如果右边界恰好位于索引 24 本身,则从右到左的 8 个连续元素将使我们到达左边界,即索引 17;因此,我们在包含范围 [17, 24] 中有 8 个元素,索引 24 处的值将是 n,它是 [17, 24] 范围内元素的总和。
这张图片甚至可以清楚地说明我上面写的内容:
重要提示:
将整数表示为 2 的不同幂次之和,源于二进制数系统的原理。
例如,1011可以写成23+21+20。
在二进制表示中,最左边的列构成 2 的 3 次方,最右边的列构成 2 的 0 次方。在二进制表示中,从最右边的列到 2 的幂每一步增加 1左边。
如果你了解二进制数制,你应该明白,当将某个数N表示为两个不同幂的和时,其中最小的数总和,与 N 的二进制表示中的部分相同,从最低有效位 (LSB) 开始并以该二进制表示的最右边数字结束,也与 2 的幂相同indexOf(LSB)-1 (如果你从右边开始用 1 索引你的二进制数)或 indexOf(LSB) (如果你用0).
这一切带来了什么?
更快的范围查询
了解范围查询在 Fenwick 树中的工作原理。
我希望您理解我们需要 前缀和 用于范围查询。
为了计算original[0, index] 的前缀总和,您现在只需在相应的Fenwick Tree 中向下级联,而不是遍历整个数组索引,并且您不断地从这些索引处的值中移除 LSB,同时继续对所有这些索引处的值求和(它们是原始数组范围的总和) .
这看起来像:
int prefixSum(int index) {
int sum = 0;
while(index!=0) {
sum+=fenwickTree[index];
index = index - LSB(index);
}
return sum;
}
问:为什么会这样?
A:我认为现在应该很明显了,但如果仍然没有 - 请密切注意我们为什么要删除 LSB(index)。我们这样做是因为在计算 前缀总和 时将 fenwickTree[index] 添加到当前总和之后,正如我们在上面已经解释的那样,存储原始数组间隔的另一个切片的下一个插槽将是在index = index - LSB(index),因为在Fenwick Tree中,indixk存储了长度为[2LSBIndexOf(toBinary(k))-1的区间sup>, k]
因此,根据我们刚刚展示的内容(级联、求和和index-LSB(index)),对于 Fenwick 树,索引 11 的前缀总和(例如)将计算为:
prefixSum = fenwickTree[11] + fenwickTree[10] + fenwickTree[8]
因为:
-
fenwickTree[11] 存储 original[11] 的总和(奇数索引仅存储这些索引处的值);
-
fenwickTree[10] 存储 original[9,10] 的总和;
-
fenwickTree[8] 存储 original[1, 8] 的总和。
你基本上有 3 个切片要总结:[1,8]、[9,10] 和 [11]。
更快的点更新
了解点更新在 Fenwick 树中的工作原理。
我认为,现在很明显 Point Update 工作的原因和方式 - 就 LSB 而言,它是范围查询的相反操作 - 而不是删除 LSB (index),您将添加 LSB(index),现在向上级联到索引并更新 Fenwick 树中的相应索引。
例如,如果我们想在索引 9 处添加一个值,您必须找出负责该索引的所有槽,并且必须更新它们。我们必须从索引 9 元素的 LSB 开始获取数字,并且必须将其添加到索引 9 处的值。我们必须不断重复此操作,直到到达 LSB 是该索引本身的数字的槽。就是这样。
void update(int i, int x) {
while (i <= n) {
fenwickTree[i] += x;
i += LSB(i); //this will give you the next slot which is used as an addend
}
}
我真的希望这对您有所帮助并阐明您的理解。