Sqrt Tree
Introduction
You are given a sequence of length \(n\): \({\left\langle a_i\right\rangle}_{i=1}^n\), and an associative operation \(\circ\) (e.g., \(\gcd,\min,\max,+,\operatorname{and},\operatorname{or},\operatorname{xor}\) are all associative). For each interval query \([l,r]\), we need to compute \(a_l\circ a_{l+1}\circ\dotsb\circ a_{r}\).
Sqrt Tree can preprocess in \(O(n\log\log n)\) time and answer queries in \(O(1)\) time.
Explanation
Sequence Blocking
First, we divide the entire sequence into \(O(\sqrt{n})\) blocks, each of size \(O(\sqrt{n})\). For each block, we compute:
- \(P_i\): prefix interval queries within the block.
- \(S_i\): suffix interval queries within the block.
- Maintain an additional array \(\left\langle B_{i,j}\right\rangle\) representing the interval answer from block \(i\) to block \(j\).
For example, suppose \(\circ\) is addition \(+\), and the sequence is \(\{1,2,3,4,5,6,7,8,9\}\).
First, we split the sequence into three blocks: \(\{1,2,3\},\{4,5,6\},\{7,8,9\}\).
Then the prefix and suffix interval answers for each block are:
The \(B\) array is:
(For illegal cases where \(i>j\), we assume the answer is 0.)
Clearly, we can preprocess these values in \(O(n)\) time, with space complexity also \(O(n)\). After preprocessing, we can use them to answer some cross-block queries in \(O(1)\) time. However, we still cannot handle queries entirely within a single block, so we need to process additional information.
Building a Tree
It is natural to think of recursively constructing the above structure within each block to support intra-block queries. For blocks of size \(1\), we can answer queries in \(O(1)\) time. This builds a tree where each node represents an interval of the sequence. Leaf nodes have interval length \(1\) or \(2\). A node of size \(k\) has \(O(\sqrt{k})\) children, so the height of the tree is \(O(\log\log n)\), and the total interval length at each layer is \(O(n)\). Therefore, constructing this tree takes \(O(n\log\log n)\) time.
Proof of Tree Height
By definition, let the subtree height of a node "controlling" \(n\) elements be \(T(n)\). We can write the recurrence:
Substitute \(n=2^m\) to get:
Define \(S(m)=T(2^m)\), then:
By the Master Theorem, \(S(m)=O(\log m)\), so \(T(n)=S(\log n)=O(\log\log n)\).
Now we can answer queries in \(O(\log\log n)\) time. For a query \([l,r]\), we only need to quickly find the node \(u\) with the smallest interval length that contains \([l,r]\), so that \([l,r]\) must span across blocks in \(u\)'s blocked interval, and we can compute the answer in \(O(1)\) time. The overall query complexity is \(O(\log\log n)\) because the tree height is \(O(\log\log n)\). However, we can still optimize this process.
Optimizing Query Complexity
We can think of binary searching the height and then checking validity in \(O(1)\) time. This reduces the complexity to \(O(\log\log\log n)\). But we can further accelerate this process.
We assume:
- Each block size is a power of two.
- Block sizes are the same within each layer.
To achieve this, we need to pad the sequence with some \(0\) elements at the end so that its length becomes a power of two. Although some blocks may double in size, they are still \(O(\sqrt{k})\), so the preprocessing complexity for blocking remains \(O(n)\).
Now we can easily determine whether a query interval is entirely contained within a single block. For interval \([l,r]\) (0-indexed), write the endpoints in binary. For example, with \(k=4, l=39, r=46\), the binary representation is:
We know the interval length at each layer is the same, and the block size is also the same (in this example, \(2^k=2^4=16\)). These blocks completely cover the entire sequence, so the first block represents elements \([0,15]\) (binary \([000000_2,001111_2]\)), the second block represents \([16,31]\) (binary \([010000_2,011111_2]\)), and so on. We observe that elements within the same block differ only in the last \(k\) bits (in this example, \(k=4\)). The example \(l\) and \(r\) also differ only in the last \(k\) bits, so they are in the same block.
Therefore, we need to check whether the two endpoints differ only in the last \(k\) bits, i.e., \(l\oplus r\le 2^k-1\). Thus, we can quickly find the layer where the answer interval lies:
- For each \(i\in [1,n]\), find the highest set bit of \(i\).
- For a query \([l,r]\), compute the highest set bit of \(l\oplus r\) to quickly determine the layer of the answer interval.
This allows us to answer queries in \(O(1)\) time.
Updating Elements
We can update elements in the Sqrt Tree, supporting both point updates and range updates.
Point Update
Consider a point assignment operation \(a_x=val\). We want to update the information efficiently.
Naive Implementation
First, let's see what happens to the Sqrt Tree after a point update.
Consider a node of length \(l\) and its corresponding sequences: \(\left\langle P_i\right\rangle,\left\langle S_i\right\rangle,\left\langle B_{i,j}\right\rangle\). It is easy to see that only \(O(\sqrt{l})\) elements change in \(\left\langle P_i\right\rangle\) and \(\left\langle S_i \right\rangle\). In \(\left\langle B_{i,j}\right\rangle\), \(O(l)\) elements change. Therefore, \(O(l)\) elements in the tree are updated. Thus, the complexity of a point update in the Sqrt Tree is \(O(n+\sqrt{n}+\sqrt{\sqrt{n}}+\dotsb)=O(n)\).
Replacing B Array with Sqrt Tree
Notice that the bottleneck of point updates is updating the root node's \(\left\langle B_{i,j}\right\rangle\). Therefore, we try to replace the root node's \(\left\langle B_{i,j}\right\rangle\) with another Sqrt Tree, called \(index\). Its function is the same as the original 2D array, maintaining the answer for the entire interval query. Other non-root nodes still use \(\left\langle B_{i,j}\right\rangle\) for maintenance. Note: if a Sqrt Tree root has an \(index\) structure, it is called indexed; if a Sqrt Tree root has a \(\left\langle B_{i,j}\right\rangle\) structure, it is called non-indexed. The \(index\) tree itself is non-indexed.
Thus, we can update the \(index\) tree as follows:
- Update \(\left\langle P_i\right\rangle\) and \(\left\langle S_i\right\rangle\) in \(O(\sqrt{n})\) time.
- Update \(index\), whose length is \(O(n)\), but we only need to update one element (representing the changed block). This step takes \(O(\sqrt{n})\) time (using the naive algorithm).
- Enter the child node where the change occurred and update its information using the naive algorithm in \(O(\sqrt{n})\) time.
Note that query complexity remains \(O(1)\) because we use the \(index\) tree at most once. Thus, point update complexity becomes \(O(\sqrt{n})\).
Updating an Interval
Sqrt Tree also supports range assignment operation \(\operatorname{Update}(l,r,x)\), i.e., setting all numbers in interval \([l,r]\) to \(x\). There are two implementations: one takes \(O(\sqrt{n}\log\log n)\) time to update information and \(O(1)\) time to query; the other takes \(O(\sqrt{n})\) time to update but increases query time to \(O(\log\log n)\).
We can use lazy propagation on the Sqrt Tree, similar to segment trees. However, there is a difference in Sqrt Tree: because pushing down a node's lazy tag may cost \(O(\sqrt{n})\), we do not push tags during queries. Instead, we check if a parent node has a tag and push it down if necessary.
First Implementation
In the first implementation, we only apply lazy tags to nodes at the first layer (nodes with interval length \(O(\sqrt{n})\)). When pushing down a tag, we update the entire subtree, costing \(O(\sqrt{n}\log\log n)\). The procedure is as follows:
- For nodes at the first layer that are completely covered by the update interval, apply a lazy tag.
- For the two blocks only partially covered, rebuild them directly in \(O(\sqrt{n}\log\log n)\) time. If they already have lazy tags from previous updates, push down the tags during rebuilding.
- Update the root node's \(\left\langle P_i\right\rangle\) and \(\left\langle S_i\right\rangle\) in \(O(\sqrt{n})\) time.
- Rebuild the \(index\) tree in \(O(\sqrt{n}\log\log n)\) time.
Now we can efficiently perform range updates. How do we answer queries using lazy tags? The procedure is:
- If the query is entirely within a block with a lazy tag, compute the answer using the tag.
- If the query spans multiple blocks, we only need the answers for the leftmost and rightmost incomplete blocks. The answers for the middle blocks can be queried from the \(index\) tree (since the \(index\) tree is rebuilt after each update) in \(O(1)\) time.
Thus, query complexity remains \(O(1)\).
Second Implementation
In this implementation, every node can have a lazy tag. Therefore, when processing a query, we need to consider lazy tags from ancestors, increasing query complexity to \(O(\log\log n)\). However, update complexity becomes faster. The procedure is:
- For blocks completely covered by the update interval, add lazy tags to them, costing \(O(\sqrt{n})\).
- For blocks partially covered, update \(\left\langle P_i\right\rangle\) and \(\left\langle S_i\right\rangle\), costing \(O(\sqrt{n})\) (since only two blocks are modified).
- Update the \(index\) tree in \(O(\sqrt{n})\) time (using the same update algorithm).
- For non-indexed subtrees, update their \(\left\langle B_{i,j}\right\rangle\).
- Recursively update the two intervals not completely covered.
Time complexity is \(O(\sqrt{n}+\sqrt{\sqrt{n}}+\dotsb)=O(\sqrt{n})\).
Implementation
The following implementation builds the tree in \(O(n\log\log n)\) time, answers queries in \(O(1)\) time, and performs point updates in \(O(\sqrt{n})\) time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
Practice Problems
This page is mainly translated from Sqrt Tree - Algorithms for Competitive Programming, with the copyright license CC-BY-SA 4.0.
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:OI-wiki
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用