Binary indexed tree
A binary indexed tree or a Fenwick tree 1 can be seen as a dynamic variant of a prefix sum array. It supports two $O(\log n)$ time operations on an array: processing a range sum query and updating a value.
The advantage of a binary indexed tree is that it allows us to efficiently update array values between sum queries. This would not be possible using a prefix sum array, because after each update, it would be necessary to build the whole prefix sum array again in $O(n)$ time.
Structure
Even if the name of the structure is a binary indexed tree, it is usually represented as an array. In this section we assume that all arrays are one-indexed, because it makes the implementation easier.
Let $p(k)$ denote the largest power of two that divides $k$. We store a binary indexed tree as an array tree such that
\[ \texttt{tree}[k] = \texttt{sum}_q(k-p(k)+1,k) \]
i.e., each position $k$ contains the sum of values in a range of the original array whose length is $p(k)$ and that ends at position $k$. For example, since $p(6)=2$, \( \texttt{tree}[6] \) contains the value of $sum_q(5,6)$.
For example, consider the following array:
The corresponding binary indexed tree is as follows:
The following picture shows more clearly how each value in the binary indexed tree corresponds to a range in the original array:
Using a binary indexed tree, any value of $\texttt{sum}_q(1,k)$ can be calculated in $O(\log n)$ time, because a range \( [1,k] \) can always be divided into $O(\log n)$ ranges whose sums are stored in the tree.
For example, the range \( [1,7] \) consists of the following ranges:
Thus, we can calculate the corresponding sum as follows: \[ \texttt{sum}_q(1,7)=\texttt{sum}_q(1,4)+\texttt{sum}_q(5,6)+\texttt{sum}_q(7,7)=16+7+4=27 \]
To calculate the value of $sum_q(a,b)$ where $a>1$, we can use the same trick that we used with prefix sum arrays:
\[ \texttt{sum}_q(a,b) = \texttt{sum}_q(1,b) - \texttt{sum}_q(1,a-1).\]
Since we can calculate both $sum_q(1,b)$ and $sum_q(1,a-1)$ in $O(\log n)$ time, the total time complexity is $O(\log n)$.
Then, after updating a value in the original array, several values in the binary indexed tree should be updated. For example, if the value at position 3 changes, the sums of the following ranges change:
Since each array element belongs to $O(\log n)$ ranges in the binary indexed tree, it suffices to update $O(\log n)$ values in the tree.
Implementation
The operations of a binary indexed tree can be efficiently implemented using bit operations. The key fact needed is that we can calculate any value of $p(k)$ using the formula \[p(k) = k \& -k.\]
The following function calculates the value of $sum_q(1,k)$:
#![allow(unused)] fn main() { let v = vec![1,3,4,8,6,1,4,2]; let tree = vec![1,4,4,16,6,7,4,29]; let mut k = 7; println!{"{:?}", tree.clone()}; println!{"k = {k}"}; let sum = sum(k, tree); println!{"{:?}", sum}; fn sum(mut k: isize, tree: Vec<isize>) -> isize{ let mut s = 0; while k>=1 { s += tree[k as usize -1]; k -= k & -k; } s } }
The following function increases the array value at position $k$ by $x$ ($x$ can be positive or negative):
#![allow(unused)] fn main() { let v = vec![1,3,4,8,6,1,4,2]; let mut tree = vec![1,4,4,16,6,7,4,29]; let mut k = 1; let mut x = 2; println!{"before: {:?}", tree.clone()}; println!{"k = {k}"}; println!{"x = {x}"}; let sum = add(&mut tree, k, x); println!{"after: {:?}", tree}; fn add(tree: &mut Vec<isize>, mut k: isize, x: isize){ while (k as usize <= tree.len()) { tree[k as usize -1] += x; k += k&-k; } } }
The time complexity of both the functions is $O(\log n)$, because the functions access $O(\log n)$ values in the binary indexed tree, and each move to the next position takes $O(1)$ time.
__
1 The binary indexed tree structure was presented by P. M. Fenwick in 1994 [21].