1. Introduction & Motivation
Square-root decomposition answers a simple question: what if you don't need the fastest possible range query, just a fast and simple one? The idea is to split an array of $n$ elements into contiguous blocks of size roughly $\sqrt{n}$. Any range operation then touches at most $O(\sqrt{n})$ full blocks, each summarized by a precomputed aggregate, plus at most two partial blocks whose elements are scanned one at a time. Both pieces cost $O(\sqrt{n})$, so the whole operation is $O(\sqrt{n})$.
That is asymptotically worse than the $O(\log n)$ a segment tree or Fenwick tree offers. The tradeoff is implementation simplicity: a block decomposition is a handful of arrays and two nested loops, with no tree recursion, no recursive build, and no subtle merge logic to get wrong. When you are under contest or interview time pressure, when the aggregate operation is unusual or hard to fit into a segment tree's merge function, or when $n$ is small enough that $\sqrt{n}$ versus $\log n$ makes no practical difference, the simpler code is often the better engineering choice. Square-root decomposition is also the foundation for other block-based techniques — most notably Mo's Algorithm for offline range queries — so the block-size intuition you build here reappears elsewhere.
2. Prerequisites
This lesson assumes comfort with basic array operations — indexing, iteration, and in-place updates — covered in Part 3: Arrays & Array ADT Operations. If point updates, linear scans, and 0-indexed slicing aren't second nature yet, that lesson is the right place to start.
You do not need to know segment trees or Fenwick trees to follow this lesson, but they are worth knowing as the $O(\log n)$ alternative to everything built here. Union-Find, Trie, Fenwick Tree & Segment Tree Decision Guide derives those structures in full; this lesson does not re-derive them, it only contrasts against them in the complexity section below.
3. A Concrete Example
Build blocks, then answer a range-sum query
Take the 9-element array [5, 2, 8, 1, 9, 3, 7, 4, 6] (0-indexed) and a block size of $\sqrt{9} = 3$. That splits the array into three blocks:
| Block | Indices | Values | Block sum |
|---|---|---|---|
| 0 | 0–2 | 5, 2, 8 | 15 |
| 1 | 3–5 | 1, 9, 3 | 13 |
| 2 | 6–8 | 7, 4, 6 | 17 |
Now answer a range-sum query for indices 1 through 7 inclusive. Index 1 falls in block 0 and index 7 falls in block 2, so the query spans three blocks:
| Block | Coverage | How it's counted | Contribution |
|---|---|---|---|
| 0 | indices 1–2 (partial) | scan elements: 2 + 8 | 10 |
| 1 | indices 3–5 (full) | use precomputed block sum | 13 |
| 2 | indices 6–7 (partial) | scan elements: 7 + 4 | 11 |
Total: $10 + 13 + 11 = 34$, which matches a direct sum of [2, 8, 1, 9, 3, 7, 4]. Block 1 was fully contained in the query, so its precomputed sum was reused instead of rescanning its three elements; blocks 0 and 2 were only partially covered, so their overlapping elements were scanned individually.
4. Core Insight
Nothing about this reasoning is specific to sums. Each block's precomputed aggregate can be the result of any associative operation — minimum, maximum, greatest common divisor, bitwise XOR, or a custom combine function — because associativity is exactly what lets a full block's aggregate stand in for its individual elements without rescanning them. Only the update rule changes: for a sum, updating one element adjusts the block total by the difference; for min or max, an increase might force a full block rescan on a point update, since the previous minimum or maximum could have been the element that changed. Sum, XOR, and GCD are especially convenient because their block aggregates update in $O(1)$ per point update.
5. Block Decomposition: Point Update & Range Query
The implementation below builds block sums in $O(n)$, supports a point update in $O(1)$ (adjust one block's total by the difference between the new and old value), and a range-sum query in $O(\sqrt{n})$ (partial scan of the two boundary blocks, plus a sum over the precomputed totals of the full blocks in between). All three languages use the same 9-element array from the worked example and reproduce the same range-sum result of 34.
6. Range Update Variants
The mirror-image problem is adding a delta across a whole range and reading back a single element. Rescanning every touched element on every range update would cost $O(n)$ per update, so instead each block keeps a pending value: a delta owed to every element of that block that hasn't been written into the underlying array yet. A range update that fully covers a block just increments that block's pending value in $O(1)$; only the two boundary blocks, which are partially covered, get their individual elements updated directly. A point query then simply adds the stored value to its block's pending delta.
7. Complexity Analysis
A segment tree answers both point updates and range queries, and both range updates (with lazy propagation) and range queries, in $O(\log n)$ — asymptotically better than $O(\sqrt{n})$ at every input size, and the gap widens as $n$ grows. This lesson does not re-derive that structure; see the linked guide for its build, update, and lazy-propagation logic.
What square-root decomposition buys instead is code you can write correctly in a few minutes: no recursive tree, no merge function to generalize, no lazy-propagation push-down order to get backwards. For competitive programming under time pressure, for a one-off script, for an unusual aggregate that doesn't compose cleanly in a segment tree's merge step, or for $n$ small enough that $\sqrt{n}$ and $\log n$ are both negligible next to constant factors, the simpler $O(\sqrt{n})$ structure is often the pragmatic choice. Reach for a segment tree when $n$ is large, when queries are frequent enough that the constant-factor difference between $\sqrt{n}$ and $\log n$ actually matters, or when you need to compose multiple range operations in one structure.
8. Applications & Alternatives
Where block decomposition shows up
- Range-sum / range-min / range-gcd queries with point updates: exactly the structure built in section 5, swapping the associative combine function.
- Range-add with point queries: the lazy-pending technique in section 6, useful whenever updates are frequent but reads are sparse.
- Offline range queries: Mo's Algorithm, a sibling lesson in this series, sorts queries by block index to answer a large batch of range queries with an amortized two-pointer sweep — it reuses this exact block-size idea, just applied to queries instead of array segments. See Mo's Algorithm for Offline Range Queries.
- Unusual aggregates: when the combine function is awkward to express as a segment-tree merge (for example, "mode of the range" or a custom frequency-based statistic), a block rebuild-on-touch strategy is often far simpler to reason about than forcing it into a tree.
9. Exercises
Easy
- Implement range-sum query with point updates using block decomposition, and verify it against a brute-force $O(n)$ scan on random inputs.
- For $n = 1{,}000{,}000$, compute the block size $\sqrt{n}$ and state how many full blocks a worst-case query would scan.
Medium
- Implement range-minimum query with point updates. Explain why a point update that increases a value may force a full block rescan, while a decrease never does.
- Extend the range-minimum structure to also return the index of the minimum, not just its value.
Hard
- Support both range updates and range queries (not just point queries) using block decomposition: combine the lazy per-block pending value from section 6 with a partial-block correction step so that a range-sum query over a mix of touched and untouched blocks still returns the right total.
- Analyze the combined structure's complexity for both operations and compare it against a segment tree with lazy propagation.
Review rhythm: tomorrow, rebuild the point-update/range-query structure from memory and re-derive why $\sqrt{n}$ is the optimal block size; next week, implement the range-update/point-query lazy variant without looking at this page; a few weeks later, combine both into the Hard exercise's range-update/range-query structure and compare its constant factors against a segment tree on the same workload.
10. Summary Cheat Sheet
- Split an array of $n$ elements into blocks of size $b \approx \sqrt{n}$; a range operation costs $O(n/b + b)$, minimized at $b = \sqrt{n}$ for $O(\sqrt{n})$ total.
- Point update / range query: $O(1)$ update (adjust one block's aggregate), $O(\sqrt{n})$ query (full blocks via precomputed aggregate, partial blocks via direct scan).
- Range update / point query: $O(\sqrt{n})$ update (partial blocks direct, full blocks via a pending delta), $O(1)$ query (stored value plus pending delta).
- The block aggregate can be any associative operation — sum, min, max, gcd, xor — not just sum.
- Segment trees and Fenwick trees give $O(\log n)$ for the same operations at the cost of more implementation complexity; choose square-root decomposition when simplicity matters more than the asymptotic gap.
- Mo's Algorithm reuses this same block-size idea to batch-process offline range queries.