这是用户在 2024-8-7 22:44 为 https://csacademy.com/lesson/fenwick_trees 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

Fenwick Tree

Suppose we have an array AA and we want to support the following two operations:

  • Update: change the value of an element AiA_i
  • Query: find the value of a certain partial sum A1+A2+...+AiA_1 + A_2 + ... + A_i

A Fenwick tree or a binary indexed tree is a data structure that handles both of these efficiently. It should be noted that if we have such a data structure, we can also find the sum over an interval [i,j][i, j] by just calculating sum(j)sum(i1)sum(j) - sum(i-1).

Brute force solutions

There are two naive solutions for this problem.

The first one is to simply maintain the array AA. For an update we need to change an element. For a query we go through all the elements of the prefix and compute their sum.

void update(int pos, int val) {
A[pos] = val;
}
int query(int pos) {
int sum = 0;
for (int i = 1; i <= pos; ++i) {
sum += A[i];
}
return sum;
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

This solution handles the update in O(1)O(1), but the query is quite slow as it works in O(N)O(N).

The second naive solution is to maintain all the partial sums. Let's say we have an array SS, where Si=A1+A2+...+AiS_i = A_1 + A_2 + ... + A_i. If we update element AiA_i we need to update all the partial sums SjS_j, where jij \geq i. In order to answer a query we only need to check a single element of SS.

void update(int pos, int val) {
int currentVal = S[pos] - S[pos - 1];
for (int i = pos; i <= N; ++i) {
S[i] += val - currentVal;
}
}
int query(int pos) {
return S[pos];
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

This second naive solution handles the update in O(N)O(N), but the query is fast and works in O(1)O(1).

Interpreting the naive solutions

In a way, these two solutions complement each other. One is performing the update fast and the query slow, while the other one does the opposite. The goal of a faster data structure is to find some sort of middle ground.

It's helpful to consider that in each of these naive solutions we maintain for each position ii an interval that ends in that position. In the first solution we maintain the interval [i,i][i, i], of length 11, while in the seconds solution we maintain the interval [1,i][1, i], of length ii.

When we want to calculate the sum of all values up to a certain index, in both solutions we keep adding the sum for the interval ending at our current index, and then skip to the first value that's not in our interval.

When we update the value at a certain index, we need to update all intervals that contain that index.

Fenwick tree structure

To improve on the previous solutions, we need to have a balance between long and short intervals, so we can skip ever increasing intervals and we don't need to update too many intervals when a value changes. If all intervals have the same length for instance, the best balance is when we use N\left \lfloor{\sqrt N}\right \rfloor as the length of our intervals. This is a common technique when you know one of your operations takes BB steps, and the other one N/BN/B steps, we would pick B=NB=\left \lfloor{\sqrt N}\right \rfloor, to minimize the worst case.

But we can get even better results if we consider the length of the interval ending at ii to be the largest power of 22 that's a divisor of ii.

Below you can see a graphical representation of the intervals, for N=16N=16:

12345678910111213141516

These intervals may seem a bit chaotic at a first glance.

Let's say FiF_i is the length of the interval ending at index ii. Basically are dealing with intervals of the form [iFi+1,i][i-F_i+1, i]. For the example above F=[1,2,1,4,1,2,1,8,1,2,1,4,1,2,1,16]F=[1, 2, 1, 4, 1, 2, 1, 8, 1, 2, 1, 4, 1, 2, 1, 16].

We can show how to build FF incrementally over lengths that are powers of 22.

  • If N=1N = 1, it only makes sense to maintain an interval of length 11, so F=[1]F=[1]:

1

  • The next step is for N=2N=2. We will keep track of the first element and the sum of the first two, F=[1,2]F=[1, 2]:

12

  • To find FF for the next power of 22, we take the previous FF, append a copy of it, and change the last element to be equal to NN. So in the case of N=4N=4, appending a copy to the previous FF means we get [1,2,1,2][1, 2, 1, 2], but then we change the last element, so the new F=[1,2,1,4]F=[1, 2, 1, 4]

1234

  • For N=8N=8, we have F=[1,2,1,4,1,2,1,8]F=[1, 2, 1, 4, 1, 2, 1, 8]

12345678

  • And for N=16N=16, we get our result F=[1,2,1,4,1,2,1,8,1,2,1,4,1,2,1,16]F=[1, 2, 1, 4, 1, 2, 1, 8, 1, 2, 1, 4, 1, 2, 1, 16].

But where is the tree?

So far, this data structures seems to be just about maintaining some interval sums, there's no explicit tree involved. But at a closer look, we can see that any two intervals are either disjoint, or one of them is completely contained by the other. This means we can interpret the set of intervals as a rooted tree (actually a forest), where the father of an interval is the shortest interval that contains it.

For example, the father of 22 (the interval ending at index 22) is 44, the father of 77 is 88, and the father of 1212 is 1616. Below, you can hover over an element to highlight its interval and its father interval, in case it exists.

12345678910111213141516

Now that we identified the hidden tree structure, there are two important details we need to figure out.

Interval length

First, given an index ii, how do you calculate the length of the interval ending at ii? We defined it as the largest power of 22 that's a divisor of ii. It would be nice to have a formula though. This value is know as the least significant bit, abbreviated lsb, of ii's representation. We can write the following function:

int length(int pos) {
int bit = 0;
while ((pos >> bit) & 1 == 0) {
++bit;
}
return (1 << bit);
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

This can take O(logN)O(log N) steps, but fortunately though, there is a faster way of finding this value, and it's all got to do with binary representation of negative numbers. To represent a negative value x-x, we take the binary representation of xx, complement it, and then add 11 (ignoring the final carry). Let's take an example where the numbers are represented on 88 bits for simplicity:

x = 01101100
~x = 10010011
-x = 10010100
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Notice the bits more significant than the lsb are different for xx and x-x, while the lsb and the following 00's are the same. Therefore, x&xx \& {-x} gives the wanted answer, in our case:

x & -x = 00000100
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Now we have a method of finding the length of any interval in O(1)O(1):

int lsb(int pos) {
return pos & -pos;
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Finding all intervals that contain a certain index

Since we've seen that intervals are either disjoint or fully contained one inside another, this part is equivalent with repeatedly just finding the next smallest interval that contains our own. Notice the father of an interval is always identified by a higher index, so we can say we need to find the difference between ii and its father.

If ii is a power of 22, the difference is equal to ii, because the father is the next power of 22. Otherwise, we can again make use of the inductive way the Fenwick tree is build, and conclude that the difference for ii is equal to the difference for ipi-p (pp is the largest power of 22, smaller than ii).

You can already guess where this is going: the difference between an interval ii and its father is equal to the interval's length, which we already know how to compute really fast.

When we change to updating the interval ending at pospos, its father, its father's father, and so on.

In the function below we consider the first parameter to be the updated position, and the second one the difference between the new value and the old value being updated:

void update(int pos, int val) {
while (pos <= N) {
fenwick[pos] += val;
pos += lsb(pos);
}
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The query

As for the query, we start at the index being queried, add the sum of its interval, and move to the left. This is where knowing the length of the intervals comes in handy:

int query(int pos) {
int sum = 0;
while (pos > 0) {
sum += fenwick[pos];
pos -= lsb(pos);
}
return sum;
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Complexity analysis

There are two important properties that help us:

  • The father of an interval is at least double in size
  • The interval to the left of any interval is at least double in size

This means we will perform at most logN\log N steps in each of the two operations, so the complexity is O(logN)O(\log N). It seems we found the middle ground we were looking for.

7 comments
You need to login to send a comment.
6 years ago
leloy

Hi! I think you forgot to return the sum in the query function

4
2
6 years ago
Jam

Very helpful post.

8
0
7 years ago
alex.velea

@Fury73, you can go to the archive and select the Fenwick Tree tag from the left, if you want to see all problems related to a specific data structure

12
0
7 years ago
Fury73

can u put some problems need this data structure and not just sum problem

11
0
7 years ago
ekzhang

In fact, if you index the nodes of a binomial heap in postorder traversal, and store the array value of each index at the node, then the fenwick tree is really just storing the subtree sums in an array.

9
0
7 years ago
ekzhang

One interesting sidenote: The tree structure generated by the nested set of intervals in a fenwick tree is actually the same structure as a binomial heap!

9
0
7 years ago
eliyanov

These lessons are fantastic. I've been looking for something like this for 2 years.

16
0