1. Introduction
A Fenwick tree, also called a binary indexed tree (BIT), is a data structure that can efficiently update elements and calculate range sums on a list of numbers. This tutorial will show how to construct a Fenwick tree to solve a mutable range sum query problem.
2. Problem Description
Let’s start with a mutable range sum query problem. Given an array of numbers with starting index of
, we have two operations:
and
. For an
operation,
, we change the value of the element at the index
to be
. For a
operation,
, we calculate the sum of numbers between positions
and
inclusive. Our goal is to implement these two operations efficiently.
3. Brute Force Solution
For the operation, we can directly update the array element at the specified location. For the
operation, we can iterate the array from index
to
and sum each element:
// In both functions:
// A = a number array with size n
function Update(i, value):
// INPUT
// i = index in array A where update occurs
// value = the new value to update at index i
// OUTPUT
// Updates array A
A[i] <- value
function Sum(left, right):
// INPUT
// left = the left index of the range to sum
// right = the right index of the range to sum
// OUTPUT
// The sum of elements between left and right in the array A
result <- 0
for i <- left to right:
result <- result + A[i]
return result
In this solution, the operation takes
time and
operation takes
time. The space requirement is
.
4. Prefix Sum Solution
In order to have a faster operation, we can use the prefix sum approach. For the input array of numbers
, the prefix sum at index
is the sum of the prefix numbers ending at
:
Based on the prefix sum definition, we can calculate a range sum with two prefix sums:
Given an array of numbers , let’s first construct a prefix sum array from
:
algorithm PrefixSum(A, n):
// INPUT
// A = a number array with size n
// OUTPUT
// the prefix sum array of A
prefix[0] <- 0
for k <- 1 to n:
prefix[k] <- prefix[k - 1] + A[k]
return prefix
For the operation, we can directly answer the query with two prefix sum values. However, we need to update the prefix sum array for the
operation:
// In both functions:
// A = a number array with size n
function update(i, value):
// INPUT
// left = the left index of the range to sum
// right = the right index of the range to sum
// OUTPUT
// The sum of elements between left and right in the array A using prefix sum
delta <- value - A[i]
for k <- i to n:
prefix[k] <- prefix[k] + delta
A[i] <- value
function sum(left, right):
// INPUT
// left = the left index of the range to sum
// right = the right index of the range to sum
// OUTPUT
// The sum of elements between left and right in the array A using prefix array
return prefix[right] - prefix[left - 1]
In this solution, the operation takes
time and
operation takes
time. The space requirement is
as we need an array to store prefix sums.
5. Fenwick Tree Solution
For the brute force and prefix sum solutions, either or
operation takes
time. If there are multiple
and
operations, these two solutions will take a lot of time. For example, if we have
numbers of
and
operations, the overall time complexity will be
for both solutions.
We can use a Fenwick tree to solve this problem more efficiently, as it only takes time for both
and
operations. Therefore, if we have
numbers of
and
operations, the overall time complexity will be
.
5.1. Range of Responsibility
When we make an operation, we have an input index parameter,
, to indicate the update element location. Similarly, when we make a
operation, we have an input index parameter,
, to indicate the sum of the prefix numbers ending at
. In the Fenwick tree solution, we do both operations based on the binary form of the input index
. That’s the reason why we also call it a binary index tree.
In a Fenwick tree, each index has a range of responsibilities. We calculate the range of responsibility value based on the position of the rightmost set bit (RSB) in the binary representation of the index. For example, the binary representation of is
. The range of responsibility of
is then
as its RSB gives value
. Another example of a range of responsibilities is for the number
. The range of responsibility of
is
as its RSB gives value
.
5.2. Range of Responsibility Calculation
In computer science, we use the two’s complement method to produce a negative number from a positive number. Firstly, we reverse the ones and zeros of the binary representation of the positive number. Then we add one. Therefore, we can calculate the range of responsibility value of an index with the bit operation:
.
For example, the binary value of in an 8-bit form is
. After we reverse each bit, we have
. When we add
, every bit
starting at the right will overflow into the next higher bit until we reach a
bit, which is the RSB of the original positive number. Therefore, we can have
.
When we do bitwise AND operation on the two numbers, we can get the range of responsibility value: .
Let’s define the range of responsibility calculation, , for an index
:
algorithm RSB(i):
// INPUT
// i = an index number
// OUTPUT
// Range of responsibility of i
return i & (-i)
5.3. Fenwick Tree Structure
The basic idea of the Fenwick tree solution is to precompute the responsibility range sum for each index. Then, we can calculate the prefix sum of an index based on the precomputed Fenwick values. Also, when we update the element at an index
, we can only update those precomputed Fenwick values that cover the index
.
Let denote the responsibility range sum for each index
. For the array
, we can have have an array
where
The following table shows the Fenwick values for numbers from 1 to 16:
Number
Binary Representation
RSB
Fenwick
Based on the Fenwick value definition, we can see that one Fenwick value can cover several Fenwick values. For example, covers
,
and
. We can use a tree structure to represent the coverage relationships:
In this Fenwick tree structure, leaf nodes are the index numbers. Each internal node represents a Fenwick value that can be constructed from its children Fenwick values and its own index array number. For example:
5.4. Fenwick Tree Construction
To construct a Fenwick tree, we can use a dynamic programming approach by propagating the smaller Fenwick values to the big Fenwick values:
algorithm ConstructFenwick(A, n):
// INPUT
// A = a number array with size n
// OUTPUT
// fenwick = the Fenwick array of A
fenwick <- A
for i <- 1 to n:
parent <- i + RSB(i)
if parent <= n:
fenwick[parent] <- fenwick[parent] + fenwick[i]
return fenwick
In this algorithm, we first initialize the Fenwick array with the input array . Then, we use a loop to propagate the smaller Fenwick values to their parent Fenwick values. The time complexity is
.
We only need to construct the Fenwick array once at the beginning. In the future, we only need to update at most Fenwick values in an
operation.
5.5. Prefix Sum Calculation
When we compute the prefix sum of index , we can start from index
and go downwards until we reach
:
algorithm PrefixSum(i):
// INPUT
// i = an index
// fenwick = the Fenwick tree of array A
// OUTPUT
// The prefix sum of A ending at index i
result <- 0
while i > 0:
result <- result + fenwick[i]
i <- i - RSB(i)
return result
In this algorithm, we start from the input index and add its Fenwick value to the
. Then, we remove the
of
and add the new Fenwick value into the
. We keep doing this process until we reach index
.
Suppose we want to find the prefix sum for index . Firstly, we add
into our
. Then, we subtract the
value of
, which is
, and reach a new index
. Therefore, we add
into our
. Again, we subtract
from
and get a new index
. Similarly, we add
into our
. Finally, we subtract
from the current index
and stop the loop as we reach the index
.
In the end, we can have .
The time complexity of this algorithm is as we remove one set bit of the original index
in each iteration until we reach
.
5.6. Fenwick Tree Update
The operation is the opposite of
operation. We start from the input index
and go upwards along the tree path to propagate the
operation.
Suppose we want to add a value at the location
. We can first add
to
. Then, we get its parent based on the
and reach the index
. Therefore we also add
to
. Similarly, we continue to update the parent Fenwick values like
and
until we reach the array size boundary
:
algorithm UpdateFenwickTree(i, delta):
// INPUT
// i = an index to update
// fenwick = the Fenwick tree of array A
// delta = the value to add
// OUTPUT
// Fenwick tree is updated with the delta value at index i
while i <= n:
fenwick[i] <- fenwick[i] + delta
i <- i + RSB(i)
Similar to the , the time complexity of
is
.
5.7. Fenwick Tree Solution
We can combine the above functions to solve the mutable range sum query problem:
// In both functions:
// A = a number array with size n
function update(i, value):
// INPUT
// i = index in array A where update occurs
// value = the new value to update at index i
// OUTPUT
// Updates the Fenwick tree and array A
delta <- value - A[i]
add(i, delta)
A[i] <- value
function sum(left, right):
// INPUT
// left = the left index of the range to sum
// right = the right index of the range to sum
// OUTPUT
// The sum of elements between left and right in the array A using Fenwick Tree
return prefixSum(right) - prefixSum(left - 1)
In the function, we first calculate the
of the updated value, Then, we call the
function to update the Fenwick tree. The time complexity is
as we only call the
function once.
In the function, we call
function twice to get the sum between
and
. The overall time complexity is still
.
The space complexity of the Fenwick solution is as we need to store the Fenwick values for all the indexes.
6. Conclusion
In this article, we showed how to use a Fenwick tree to solve the mutable range sum problem. With the Fenwick tree, we can achieve time for both
and
operations.