Sqrt-tree: answering queries in O(1) with O(loglogN) preprocessing.

Revision en4, by gepardo, 2018-01-11 21:58:21

Hello, Codeforces!

Some time ago I invented an interesting data structure and I'd like to share it with the community. Maybe, it was known before, and if you knew about it before my blog post, please share the link to the source.

# What can the data structure do?

Given an array a that contains n elements and the operation op that satisfies some properties:

1. Associative property: (x op y)op z = x op(y op z).
2. Commutative property (optional, not required): x op y = y op x.

So, such operations as gcd, min, max, sum, multiplication, and, or, xor, etc. satisfy these conditions.

Also we have some queries l, r. For each query, we need to find al op al + 1 op ... op ar. Let's call such queries q(l, r).

My data structure can process such queries in O(1) time with preprocessing time and memory.

# How it works?

## 1. Sqrt

Let's do a sqrt-decomposition. We divide our array in blocks, each block has size . For each block, we compute:

1. Answers to the queries that lie in the block and begin at the beginning of the block (prefix-op)
2. Answers to the queries that lie in the block and end at the end of the block (suffix-op)

And we'll calculate another one thing:

1. between[i, j] i ≤ j -- answer to the query that begins at the start of block i and ends at the end of block j. Note that we have blocks, so the size of this array will be .

Let's see the example.

Let op be  +  (we calculate sum on the segment) and we have the following array a:

1 2 3 4 5 6 7 8 9

It will be divided onto three blocks: 1 2 3, 4 5 6 and 7 8 9.

For first block prefix-op is 1 3 6 and suffix-op is 6 5 3.

For second block prefix-op is 4 9 15 and suffix-op is 15 11 6.

For third block prefix-op is 7 15 24 and suffix-op is 24 17 9.

between array is:

6 21 45
- 15 39
-  - 24


It's obvious to see that these arrays can be easily calculated in O(n) time and memory.

We already can answer some queries using these arrays. If the query doesn't fit into one block, we can divide it onto three parts: suffix of a block, then some segment of contiguous blocks and then prefix of some block. We can answer a query by dividing it into three parts and taking op of some value from suffix-op, then some value from between, then some value from prefix-op.

But if we have queries that entirely fit into one block, we cannot process them using these three arrays. So, we need to do something.

## 2. Tree

We cannot answer only the queries that entirely fit in one block. But what if we build the same structure as described above for each block? Yes, we can do it. And we do it recursively, until we reach the block size of 1 or 2. Answers for such blocks can be calculated easily in O(1).

So, we get a tree. Each node of the tree represents some segment of the array. Node that represents array segment with size k has children -- for each block. Also each node contains the three arrays described above for the segment it contains. The root of the tree represents the entire array. Nodes with segment lengths 1 or 2 are leaves.

Also it's obvious that the radius of this tree is , because if some vertex of the tree represents an array with length k, then its children have length . , so decreases two times every layer of the tree and so its height is . The time for building and memory usage will be , because every element of the array appears exactly once on each layer of the tree.

Now we can answer the queries in . We can go down on the tree until we meet a segment with length 1 or 2 (answer for it can be calculated in O(1) time) or meet the first segment in which our query doesn't fit entirely into one block. See the first section on how to answer the query in this case.

OK, now we can do per query. Can it be done faster?

## 3. Optimizing the query complexity

One of the most obvious optimization is to binary search the tree node we need. Using binary search, we can reach the complexity per query. Can we do it even faster?

The answer is yes. Let's assume the following two things:

1. Each block size is a power of two.
2. All the blocks are equal on each layer.

To reach this, we can add some zero elements to our array so that its size becomes a power of two.