Introduction
This blog is a continuation of part 1. In this post, I will explain an optimization to the SQRT Decomposition part.
Prerequisites
Before reading this post, you should be familiar with:
- part 1
- predecessor problem
- fractional cascading
Recap
We want a data structure that works under $$$Z_{2^i}$$$ and allows us to add value k to a range, and query the number of elements within a specific value range inside a given index range.
SQRT Decomposition
Definitions
First let $$$T_p$$$ be the time complexity of a predecessor data structure we want to use(We need to assume that we use a reasonable structure, not worse than BBST). For example, if we use y-fast trie $$$T_p = \mathcal{O}(\log\log \text{MAX})$$$.
Notice that if we just store a predecessor data structure in each block of a previous SQRT decomposition, we achieve complexity $$$\mathcal{O}(\sqrt{n}\cdot T_p)$$$. This is not ideal. We aim to achieve $$$\mathcal{O}(\sqrt{N\cdot T_p})$$$.
Decomposition
Our decomposition is two-dimensional. First we split an array into blocks of size $$$\sqrt{\frac{N}{T_p}}$$$ and then we group these blocks into groups of size $$$T_p$$$. For each group we store a lazy value.
Each group is maintained as a fractional cascading structure over its blocks. For the first block(the root of the cascading), we store a predecessor data structure.
Count
We iterate over all groups and from a fractional cascading considering the lazy value. Thanks to this we know for all blocks in this group how many elements in the given range are in this group in $$$\mathcal{O}(T_p)$$$. Blocks that are partially inside our query are processed naively. This gives us a complexity of $$$\mathcal{O}(\sqrt{\frac{N}{T_p}}\cdot T_p + \sqrt{\frac{N}{T_p}}) = \mathcal{O}(\sqrt{N\cdot T_p})$$$.
Add
For each group fully inside our query, we update the lazy value.
We have at most 2 border groups (groups partially covered by the query). For each of them, we need to recompute the fractional cascading structure:
- Partial Blocks: The specific blocks containing the query boundaries ( l and r) are recalculated naively. We can re-sort them using the predecessor data structure in $$$\mathcal{O}(\text{size}\cdot T_p)$$$.
- Full Blocks: The blocks inside the group that are not cut by the query boundaries are easy to sort. Since we are in $$$Z_{2^i}$$$, adding a value results in a cyclic shift, which we can fix in $$$\mathcal{O}(\text{size})$$$.
- Merge: Finally, we rebuild the fractional cascading links for the group.
This takes $$$\mathcal{O}(\sqrt{N\cdot T_p})$$$.








