Introduction
Given a sequence of length n, , and an associative operation (for example, are all associative), for each range query , we need to compute .
A Sqrt Tree can preprocess in time and answer queries in time.
Explanation
Blocking the sequence
First, we divide the entire sequence into blocks, each of size . For each block, we compute:
- The prefix range-query results within the block,
- The suffix range-query results within the block,
- An additional array that stores the range-query result from block through block .
For example, suppose that is addition, , and the sequence is .
We first divide the sequence into three blocks, obtaining .
The prefix and suffix range-query results for each block are then
The array is:
(For the invalid case , we assume that the answer is 0.)
Clearly, these values can be preprocessed in time, with the same space complexity. Once they are ready, we can use them to answer some cross-block queries in time. However, we still cannot handle queries whose entire interval lies within one block, so we need to process some additional information.
Building a tree
It is natural to recursively construct the structure above within each block to support queries inside a block. Queries for a block of size can be answered in time. This gives us a tree in which each node represents an interval of the sequence. Leaf-node intervals have length or . A node of size has children, so the height of the entire tree is . The total length of the intervals on each level is , so the construction complexity is .
Proof of the tree height
By definition, let be the number of elements controlled by a node, and let the height of its subtree be . We can write the recurrence:
Substitute to obtain
Now define . Substitution gives
By the master theorem, , and therefore .
We can now answer queries in time. For a query , we only need to quickly find a node with the smallest possible interval such that contains . Then must cross blocks in the block decomposition of , so its answer can be computed in time. The total complexity of one query is because the tree height is . However, this process can still be optimized.
Optimizing query complexity
It is easy to think of binary-searching the height and checking validity in time. This reduces the complexity to . However, we can accelerate the process further.
We assume that
- The size of every block is an integral power of ;
- The block sizes on the same level are equal.
To achieve this, we append some elements to the end of the sequence so that its length becomes an integral power of . Although some blocks may become twice as large as before, they are still of size , so the preprocessing complexity for the blocks remains .
We can now easily determine whether a query interval is entirely contained in one block. For an interval (zero-indexed), write the endpoints in binary. For example, for , the binary representations are
We know that the interval lengths on each level are equal, and that the block sizes are also equal (in the example above, ). These blocks cover the entire sequence, so the first block represents the elements (in binary, ), while the second block represents the interval (in binary, ), and so on. We see that the positions of elements in the same block differ only in their last bits (where in the example). The example's also differ only in their last bits, so they belong to the same block.
Therefore, we need to check whether the two endpoints of the interval differ only in their last bits, that is, whether . We can then quickly find the layer containing the answer interval:
- For each , find , then take its highest-order bit;
- For a query , compute the highest-order bit of to quickly determine the layer containing the answer interval.
This lets us answer queries in time.
Updating elements
Elements can be updated on a Sqrt Tree; both point updates and range updates are supported.
Point updates
Consider a point assignment . We want to update the information affected by this operation efficiently.
Naive implementation
First, let us see what happens to a Sqrt Tree after a point update.
Consider a node of length and its corresponding sequences: . It is easy to see that in each of and , only elements change. In , however, elements change. Thus, elements are updated in the tree. Therefore, the complexity of a point update on a Sqrt Tree is .
Using a Sqrt Tree instead of the B array
The bottleneck of a point update is updating the root node's . Therefore, we try replacing the root node's with another Sqrt Tree, called . Its role is the same as that of the original two-dimensional array: it stores the answers to queries over entire ranges. Other non-root nodes continue to use . If the root node of a Sqrt Tree has an structure, we call that Sqrt Tree indexed; if the root node has a structure, we call it unindexed. The tree itself is unindexed.
We can update the tree as follows:
- In time, update and .
- Update . Its length is , but we only need to update one of its elements (the element representing the changed block), so this step takes time using the naive algorithm.
- Descend into the changed child and update its information in time using the naive algorithm.
The query complexity remains because we use the tree at most once. Therefore, the complexity of a point update is .
Updating a range
A Sqrt Tree also supports range assignment operations , which set every number in the interval to . There are two implementation approaches. One updates the information in time and answers queries in time; the other updates the information in time, but increases query time to .
We can add lazy tags to a Sqrt Tree just as in a segment tree. There is one difference on a Sqrt Tree: propagating a node's lazy tag may take time. Therefore, instead of propagating tags while answering a query, we check whether the parent node has a tag and propagate it if it does.
First implementation
In the first implementation, we add lazy tags only to nodes on level (nodes whose intervals have length ). When propagating a tag, we update the entire subtree directly, with complexity . The procedure is as follows:
-
Consider the nodes on level . Add a lazy tag to every node completely contained in the update interval;
-
Two blocks are only partially covered. We directly rebuild these two blocks in time. If either already has a lazy tag from an earlier update, propagate the tag during the rebuild;
-
Update the root node's and in time;
-
Rebuild the tree in time.
We can now perform range updates efficiently. How do we use lazy tags to answer queries? The procedure is:
-
If our query is contained within a block with a lazy tag, use the tag to compute the answer;
-
If our query contains multiple blocks, we only need to consider the answers for the leftmost and rightmost incomplete blocks. The answers for the blocks in between can be queried in the tree (because the tree is rebuilt after every update), in time.
Thus, query complexity remains .
Second implementation
In this implementation, every node can receive a lazy tag. Therefore, when processing a query, we need to account for lazy tags among its ancestors, making the query complexity . The complexity of updating the information becomes lower, however. The procedure is:
- For blocks completely contained in the update interval, add a lazy tag to those blocks in time;
- For blocks partially covered by the update interval, update and in time (there are only two such blocks);
- Update the tree in time (using the same update algorithm);
- For unindexed subtrees, update their ;
- Recursively update the two intervals that are not completely covered.
The time complexity is .
Implementation
The implementation below builds the tree in time, answers queries in time, and performs point updates in time.
SqrtTreeItem op(const SqrtTreeItem &a, const SqrtTreeItem &b);
int log2Up(int n) {
int res = 0;
while ((1 << res) < n) {
res++;
}
return res;
}
class SqrtTree {
private:
int n, lg, indexSz;
vector<SqrtTreeItem> v;
vector<int> clz, layers, onLayer;
vector<vector<SqrtTreeItem>> pref, suf, between;
void buildBlock(int layer, int l, int r) {
pref[layer][l] = v[l];
for (int i = l + 1; i < r; i++) {
pref[layer][i] = op(pref[layer][i - 1], v[i]);
}
suf[layer][r - 1] = v[r - 1];
for (int i = r - 2; i >= l; i--) {
suf[layer][i] = op(v[i], suf[layer][i + 1]);
}
}
void buildBetween(int layer, int lBound, int rBound, int betweenOffs) {
int bSzLog = (layers[layer] + 1) >> 1;
int bCntLog = layers[layer] >> 1;
int bSz = 1 << bSzLog;
int bCnt = (rBound - lBound + bSz - 1) >> bSzLog;
for (int i = 0; i < bCnt; i++) {
SqrtTreeItem ans;
for (int j = i; j < bCnt; j++) {
SqrtTreeItem add = suf[layer][lBound + (j << bSzLog)];
ans = (i == j) ? add : op(ans, add);
between[layer - 1][betweenOffs + lBound + (i << bCntLog) + j] = ans;
}
}
}
void buildBetweenZero() {
int bSzLog = (lg + 1) >> 1;
for (int i = 0; i < indexSz; i++) {
v[n + i] = suf[0][i << bSzLog];
}
build(1, n, n + indexSz, (1 << lg) - n);
}
void updateBetweenZero(int bid) {
int bSzLog = (lg + 1) >> 1;
v[n + bid] = suf[0][bid << bSzLog];
update(1, n, n + indexSz, (1 << lg) - n, n + bid);
}
void build(int layer, int lBound, int rBound, int betweenOffs) {
if (layer >= (int)layers.size()) {
return;
}
int bSz = 1 << ((layers[layer] + 1) >> 1);
for (int l = lBound; l < rBound; l += bSz) {
int r = min(l + bSz, rBound);
buildBlock(layer, l, r);
build(layer + 1, l, r, betweenOffs);
}
if (layer == 0) {
buildBetweenZero();
} else {
buildBetween(layer, lBound, rBound, betweenOffs);
}
}
void update(int layer, int lBound, int rBound, int betweenOffs, int x) {
if (layer >= (int)layers.size()) {
return;
}
int bSzLog = (layers[layer] + 1) >> 1;
int bSz = 1 << bSzLog;
int blockIdx = (x - lBound) >> bSzLog;
int l = lBound + (blockIdx << bSzLog);
int r = min(l + bSz, rBound);
buildBlock(layer, l, r);
if (layer == 0) {
updateBetweenZero(blockIdx);
} else {
buildBetween(layer, lBound, rBound, betweenOffs);
}
update(layer + 1, l, r, betweenOffs, x);
}
SqrtTreeItem query(int l, int r, int betweenOffs, int base) {
if (l == r) {
return v[l];
}
if (l + 1 == r) {
return op(v[l], v[r]);
}
int layer = onLayer[clz[(l - base) ^ (r - base)]];
int bSzLog = (layers[layer] + 1) >> 1;
int bCntLog = layers[layer] >> 1;
int lBound = (((l - base) >> layers[layer]) << layers[layer]) + base;
int lBlock = ((l - lBound) >> bSzLog) + 1;
int rBlock = ((r - lBound) >> bSzLog) - 1;
SqrtTreeItem ans = suf[layer][l];
if (lBlock <= rBlock) {
SqrtTreeItem add =
(layer == 0) ? (query(n + lBlock, n + rBlock, (1 << lg) - n, n))
: (between[layer - 1][betweenOffs + lBound +
(lBlock << bCntLog) + rBlock]);
ans = op(ans, add);
}
ans = op(ans, pref[layer][r]);
return ans;
}
public:
SqrtTreeItem query(int l, int r) { return query(l, r, 0, 0); }
void update(int x, const SqrtTreeItem &item) {
v[x] = item;
update(0, 0, n, 0, x);
}
SqrtTree(const vector<SqrtTreeItem> &a)
: n((int)a.size()), lg(log2Up(n)), v(a), clz(1 << lg), onLayer(lg + 1) {
clz[0] = 0;
for (int i = 1; i < (int)clz.size(); i++) {
clz[i] = clz[i >> 1] + 1;
}
int tlg = lg;
while (tlg > 1) {
onLayer[tlg] = (int)layers.size();
layers.push_back(tlg);
tlg = (tlg + 1) >> 1;
}
for (int i = lg - 1; i >= 0; i--) {
onLayer[i] = max(onLayer[i], onLayer[i + 1]);
}
int betweenLayers = max(0, (int)layers.size() - 1);
int bSzLog = (lg + 1) >> 1;
int bSz = 1 << bSzLog;
indexSz = (n + bSz - 1) >> bSzLog;
v.resize(n + indexSz);
pref.assign(layers.size(), vector<SqrtTreeItem>(n + indexSz));
suf.assign(layers.size(), vector<SqrtTreeItem>(n + indexSz));
between.assign(betweenLayers, vector<SqrtTreeItem>((1 << lg) + bSz));
build(0, 0, n, 0);
}
};
Exercises
This page is primarily translated from Sqrt Tree - Algorithms for Competitive Programming, under the CC-BY-SA 4.0 license.