Fenwick Trees Made Easy!
28 July 2024
Let’s start with an array of size , each entry is initially zero. Suppose that over time, I want to support these operations on the array:
- Add all indices of in range by an integer , where
- Find the sum of , where
We can do this naively with a single for loop, right? This means each query type can be done in time. However, what if I tell you that there is a data structure that can support both these operations in time? Let me introduced you to Fenwick trees, also called as binary indexed trees (BIT)!
Update and Query
Before we begin, let me introduce you to the concept of update and query operations. The first operation mentioned in the prologue is an update operation, whereas the second one is a query operation. On specific scenarios where it is guaranteed that , we call these operations point operations, otherwise they are range operations. Therefore, Fenwick trees are capable of range updates (RU) and range queries (RQ), as well as point updates (PU) and point queries (PQ).
The Classic PURQ
I would like to dive straight to the PURQ version of the Fenwick tree which is the most commonly used version.
You can also test your own implementation of the PURQ Fenwick tree on this Kattis problem: Fenwick Tree.
The PURQ Fenwick tree utilizes another array (let’s call this array ) of size to support 1-based indexings. The main concept of Fenwick trees is the use of least significant bits (LSBs) to indicate which indices on stores the value of a particular index on the original array.
For example, the least significant of is because and therefore the LSB here is the rightmost 1-bit which is . To quickly get the LSB of , we simply evaluate , where is the binary AND operation.
Now, how do we determine which indices of that store for a particular index ?
- Start with (add 1 since the Fenwick tree array is 1-based)
- Repeat the following as long as :
- Store in
- Add by the LSB of
For example, suppose . This means is stored in . This is because and . For the rest of indices, do take a look at the following visualization below (indices of are in pink text).
Therefore, when adding by some integer , we simply do the same to all the indices of that stores .
As for the range query for , let’s start with querying the prefix sum, meaning . Similarly, we also make use of the LSB of , but instead of adding it until it exceeds , we keep subtracting until it becomes .
For example, we want to query the sum of , so we start with , and then we keep adding to our answer and subtract by until equals . Therefore, for we have as our final answer because and .
To wrap up, we can now generalize to any value of by subtracting the sum of with the sum of .
Some of you might wonder what happens if the example isn’t a power of 2, and Fenwick tree will still work in this case (image below shows the case when ).
Supporting RU
RUPQ
To support range updates optimally, we have to change the structure of to some extent. Let’s start with reimplementing such that it supports RUPQ first, then we can extend it to support RURQ.
We revisit the example when . Suppose I want to add by and by . Our actual array should look something like this:
Let’s have another array of size . To resemble adding by , we simply add by and by . Similarly, we also add by and by . So, for every operation that adds by , we add by and by .
To answer the point query operations, we simply take the prefix sum of . For example to query the value , we’re basically querying the sum of . Therefore, for every operation that queries , we simply query for the sum of .
Based on the examples above, we can use a PURQ Fenwick tree on to support RUPQ on . If we want to add by , we basically perform two point updates on as explained previously, while doing a point query on is basically performing a (prefix) range query on .
RURQ
And now, the extension from RUPQ to RURQ!
Given the original array and the transformed array as what we see on the RUPQ case, we add another transformed array to handle the extra terms.
Recall that for every operation that adds by , we add by and by . Now, we also add by and by . The rigorous explanation on why one might come up with such structure can be found in this article by CP Algorithms.
Finally, to obtain the prefix range sum of , we perform the following calculation.
This means
In conclusion, we can use two PURQ Fenwick trees on and respectively to support RURQ on . If we want to add by , we basically perform two point updates each on and as explained, while doing a range query on is basically performing TWO (prefix) range queries on and also on .
Other Binary Operations
The addition operation is not the only operation Fenwick trees can support. In fact, there are more (associative) binary operations that can be used as an example.
XOR
The main algorithm and array structure remains the same, depending whether you need the PURQ version, the RUPQ version, or the RURQ version. The only difference is that the addition operation is changed into the binary XOR operation. So, instead of adding elements by some integer , you apply XOR on these elements by .
Here’s a Kattis problem that you can try out using XOR Fenwick trees: Association of Cats and Magical Lights.
Min or Max
Though it is more advisable to support range operations involving minimum/maximum using segment trees, we can also perform such using Fenwick trees.
Since we can no longer use the prefix trick like the case for XOR and addition to answer range queries, we need to revamp things to some extent again. (Note that for addition and XOR, we can obtain given and )
We now use an array of size instead of , and the operations mainly involves simple bitshifts (or multiplication/division by 2) instead of computing LSBs like we used to do on the classic PURQ example.
More details can be seen at the Python code attached on the last section of this article.
Limitations
Since the main data structure used on Fenwick trees are arrays, we can see that it can hold up to a limited number of elements. Realistically speaking, for programs that are timed under 1 second, you can contain at most about elements. To support potentially way more elements than that, I suggest using (dynamic) segment trees which Fenwick trees are a subset of.
Similarly, some operations that are not (that) binary nor associative require segment trees to implement instead of Fenwick trees. For example, the second largest element in a range.
Python Code
I always keep my (exclusively Python) data structures and algorithms templates on this code repository: pytils.
Specifically for this article, here are two templates that would help:
- Sum update/query: PURQ, RURQ
- Min/max update/query: PURQ, RUPQ