O(log(n)^2) Range Queries on Fenwick Tree Without Inverse Operation

Revision en1, by estoy-re-sebado, 2023-10-14 21:26:09

This is not a useful blog, just something I noticed and thought to write down. It's probably been talked about before as well.

Background: Fenwick Tree

The Fenwick tree is a data structure that is commonly used to perform prefix sum queries and point updates on some array

Unable to parse markup [type=CF_MATHJAX]

. Leaving the issue of updates aside the general idea is that, for each index $$$i$$$, it stores the sum of A over the range $$$(i-f(i), i]$$$ (yes, that's an open-closed interval), where

Unable to parse markup [type=CF_MATHJAX]

returns the least significant bit of $$$i$$$.

To compute a prefix sum over

Unable to parse markup [type=CF_MATHJAX]

we start with the interval

Unable to parse markup [type=CF_MATHJAX]

(where

Unable to parse markup [type=CF_MATHJAX]

), then we add

Unable to parse markup [type=CF_MATHJAX]

(where

Unable to parse markup [type=CF_MATHJAX]

), and so on until we cover the entire range. Since each

Unable to parse markup [type=CF_MATHJAX]

has one fewer bit than

Unable to parse markup [type=CF_MATHJAX]

, this can only happen

Unable to parse markup [type=CF_MATHJAX]

times in the worst case. Therefore any prefix sum query is computed in

Unable to parse markup [type=CF_MATHJAX]

worst case running time.
int fenwick_tree[MAXN];
int prefix(int i) {
  int x = 0;
  for (; i > 0; i -= f(i)) x += fenwick_tree[i];
  return x;
}

It is also easy to compute any range sum query over a range

Unable to parse markup [type=CF_MATHJAX]

by taking the sum over the prefix

Unable to parse markup [type=CF_MATHJAX]

and subtracting

Unable to parse markup [type=CF_MATHJAX]

.
int range(int l, int r) {
  return prefix(r) - prefix(l-1);
}

Generalizing

As presented above, instead of sums, we could compute prefix queries for any associative operation, and we could compute range queries for any associative and commutative operation that has an inverse. The requirement on commutativity can be easily removed at the expense of more memory usage and longer implementation. The requirement of an inverse, however, is inherent to the idea of computing ranges by using two overlapping prefixes but is not really necessary to achieve range queries in some other way.

Range queries without inverse

Given this data structure, there is a simple greedy algorithm for computing range queries over

Unable to parse markup [type=CF_MATHJAX]

:
  • if

    Unable to parse markup [type=CF_MATHJAX]

    is contained in

    Unable to parse markup [type=CF_MATHJAX]

    , accumulate that range and then compute $$$[l, r-f(r)]$$$
  • otherwise, accumulate

    Unable to parse markup [type=CF_MATHJAX]

    and then compute

    Unable to parse markup [type=CF_MATHJAX]

It can be shown that this algorithm runs in $$$O(\log(n)^{2})$$$ in the worst case.

I will not write a proof but the intuition is that $$$f(i - f(i))$$$ is twice

Unable to parse markup [type=CF_MATHJAX]

(or zero). That means that in $$$O(\log n)$$$ steps we will cover any interval. We may overshoot, but by that time we will have covered at least half of the interval already, so this process can only happen $$$O(\log n)$$$ times.

The code looks something like this:

int range(int l, int r) {
  int x = 0;
  while (r-l+1 > 0) {
    if (f(r) <= r-l+1) {
      x += fenwick_tree[r]; r -= f(r);
    } else {
      x += A[r]; r -= 1;
    }
  }
  return x;
}

I used the sum operation for clarity, but it also works for any associative operation with an identify element.

int range(int l, int r) {
  int x = IDENT;
  while (r-l+1 > 0) {
    if (f(r) <= r-l+1) {
      x = OP(fenwick_tree[r], x); r -= f(r);
    } else {
      x = OP(A[r], x); r -= 1;
    }
  }
  return x;
}

For instance, we could use it with min operation:

#define IDENT INT_MAX
#define OP min

Conclusion

The Fenwick tree can perform arbitrary range queries on associative operations but if we want to do any kind of updates at all, we will need commutative operations. Also, if we want to perform point-assignment updates -- which is the most common for RMQ among others -- we would need an inverse operation.

It has slow complexity compared to e.g. segment trees, and in practice i tested it to be even slower that the obvious sqrt-decomposition approach.

The only situation where I would use this is if we were forced to use the input array in-place, as the Fenwick tree uses exactly N words of memory and is not that hard to build in-place over an existing array.

Thus I conclude that this is not a useful data structure at all.

Tags range query, fenwick tree

History

 
 
 
 
Revisions
 
 
  Rev. Lang. By When Δ Comment
en2 English estoy-re-sebado 2023-10-14 21:28:16 8 Fix a typo
en1 English estoy-re-sebado 2023-10-14 21:26:09 4039 Initial revision (published)