Square-root decomposition
Square root decomposition cuts an array into blocks of about sqrt(n) and keeps one summary per block, giving simple O(sqrt n) queries and updates for problems that are awkward for trees.
Learning goals
- Build block sums and choose the block size
- Update a point and answer a range query by combining whole blocks and partial blocks
- Make range updates cheap with lazy block tags
- Weigh rebuilding against incremental updates
Prerequisites
Cut a book into chapters. To count words from page 40 to page 260, add the stored totals of the whole chapters and count the few pages at the two ends by hand. The best chapter length balances the number of chapters against the pages per chapter: about the square root of the page count.
Block sums
Split n elements into blocks of size B, so block b covers indices b × B to (b + 1) × B - 1 (the last may be shorter), and store the sum of each block. There are about n / B blocks. A range query costs at most 2B single elements at the two ends plus n / B block sums, which is smallest when 2B ≈ n / B, that is B ≈ sqrt(n / 2); in practice B = sqrt(n) is used.
In the animation's default input, 12 values give B = ceil(sqrt 12) = 4 and 3 blocks. Each block is drawn as a row with its sum. The code below also shows how to find the best block size by direct search of the cost 2B + n / B.
1class SqrtBlocks {2final int B;3final int[] a, sum;4SqrtBlocks(int[] values) {5a = values.clone();6int n = a.length;7B = (int) Math.ceil(Math.sqrt(n));8sum = new int[(n + B - 1) / B];9for (int i = 0; i < n; i++) sum[i / B] += a[i]; // block of index i is i / B10}11static int bestBlockSize(int n) { // minimises 2B + ceil(n / B), the cost of a worst-case query12int best = 1;13for (int b = 1; b <= n; b++)14if (2 * b + (n + b - 1) / b < 2 * best + (n + best - 1) / best) best = b;15return best;16}17}
Square root decomposition: block sums, updates and lazy block tags
Interactive · edit the inputblock 0: sum = 21
block 1: sum = 19
block 2: sum = 15
12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps.
1int B; int[] a, sum, lazy; // block b covers a[b*B .. min(n, (b+1)*B) - 1]2void build(int[] values) {3a = values; int n = a.length; B = (int) Math.ceil(Math.sqrt(n));4sum = new int[(n + B - 1) / B]; lazy = new int[sum.length];5for (int i = 0; i < n; i++) sum[i / B] += a[i];6}7void update(int i, int v) { // point update: O(1)8int cur = a[i] + lazy[i / B];9sum[i / B] += v - cur; a[i] = v - lazy[i / B];10}11int query(int l, int r) { // inclusive12int total = 0;13for (int i = l; i <= r; ) {14if (i % B == 0 && i + B - 1 <= r) { total += sum[i / B]; i += B; } // a whole block: one step15else { total += a[i] + lazy[i / B]; i++; } // a partial block: element by element16}17return total;18}19void rangeAdd(int l, int r, int v) {20for (int i = l; i <= r; ) {21int b = i / B;22if (i % B == 0 && i + B - 1 <= r && (b + 1) * B <= a.length) { lazy[b] += v; sum[b] += v * B; i += B; } // whole block: a tag23else { a[i] += v; sum[b] += v; i++; } // partial block: touch the element24}25}
Live state
n
12
block size B
4
blocks
3
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=12 block size B=4 blocks=3 | 12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps. |
| 2 | L15 | i=2 value=1 total=1 | sum[2..9]: index 2 is in a partial block: add the element 1 (total 1). |
| 3 | L15 | i=3 value=9 total=10 | sum[2..9]: index 3 is in a partial block: add the element 9 (total 10). |
| 4 | L14 | i=4 block=1 total=29 | sum[2..9]: block 1 lies fully inside [2, 9]: add its stored sum 19 in one step (total 29). |
| 5 | L15 | i=8 value=5 total=34 | sum[2..9]: index 8 is in a partial block: add the element 5 (total 34). |
| 6 | L15 | i=9 value=0 total=34 | sum[2..9]: index 9 is in a partial block: add the element 0 (total 34). |
| 7 | L17 | return=34 elements touched=4 blocks used=1 | Sum = 34, from 1 whole block and 4 single elements. A plain loop would have read 8. |
Point update
A point update changes one element and the sum of its block: sum[i / B] += newValue - a[i]; a[i] = newValue. It costs O(1), which is cheaper than the O(log n) of a segment tree. This makes the structure attractive when updates are far more frequent than queries.
Choose "Point update" in the animation (a[5] becomes 20): the value changes from 8 to 20, so block 1's sum moves by 12, from 19 to 31. Only one element and one block sum are touched.
1class SqrtBlocks {2final int B;3final int[] a, sum;4SqrtBlocks(int[] values) {5a = values.clone();6int n = a.length;7B = (int) Math.ceil(Math.sqrt(n));8sum = new int[(n + B - 1) / B];9for (int i = 0; i < n; i++) sum[i / B] += a[i];10}11void update(int i, int v) { // O(1)12sum[i / B] += v - a[i];13a[i] = v;14}15}
Square root decomposition: block sums, updates and lazy block tags
Interactive · edit the inputblock 0: sum = 21
block 1: sum = 19
block 2: sum = 15
12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps.
1int B; int[] a, sum, lazy; // block b covers a[b*B .. min(n, (b+1)*B) - 1]2void build(int[] values) {3a = values; int n = a.length; B = (int) Math.ceil(Math.sqrt(n));4sum = new int[(n + B - 1) / B]; lazy = new int[sum.length];5for (int i = 0; i < n; i++) sum[i / B] += a[i];6}7void update(int i, int v) { // point update: O(1)8int cur = a[i] + lazy[i / B];9sum[i / B] += v - cur; a[i] = v - lazy[i / B];10}11int query(int l, int r) { // inclusive12int total = 0;13for (int i = l; i <= r; ) {14if (i % B == 0 && i + B - 1 <= r) { total += sum[i / B]; i += B; } // a whole block: one step15else { total += a[i] + lazy[i / B]; i++; } // a partial block: element by element16}17return total;18}19void rangeAdd(int l, int r, int v) {20for (int i = l; i <= r; ) {21int b = i / B;22if (i % B == 0 && i + B - 1 <= r && (b + 1) * B <= a.length) { lazy[b] += v; sum[b] += v * B; i += B; } // whole block: a tag23else { a[i] += v; sum[b] += v; i++; } // partial block: touch the element24}25}
Live state
n
12
block size B
4
blocks
3
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=12 block size B=4 blocks=3 | 12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps. |
| 2 | L15 | i=2 value=1 total=1 | sum[2..9]: index 2 is in a partial block: add the element 1 (total 1). |
| 3 | L15 | i=3 value=9 total=10 | sum[2..9]: index 3 is in a partial block: add the element 9 (total 10). |
| 4 | L14 | i=4 block=1 total=29 | sum[2..9]: block 1 lies fully inside [2, 9]: add its stored sum 19 in one step (total 29). |
| 5 | L15 | i=8 value=5 total=34 | sum[2..9]: index 8 is in a partial block: add the element 5 (total 34). |
| 6 | L15 | i=9 value=0 total=34 | sum[2..9]: index 9 is in a partial block: add the element 0 (total 34). |
| 7 | L17 | return=34 elements touched=4 blocks used=1 | Sum = 34, from 1 whole block and 4 single elements. A plain loop would have read 8. |
Range query
A query walks from l to r: whenever the current index starts a block that fits inside the range, add the stored block sum and jump B positions; otherwise add a single element and move one step. At most 2(B - 1) single elements are read (the partial blocks at the two ends) plus at most n / B whole blocks.
In the preset "Range sum across blocks" the query [2, 9] on 12 values reads 4 single elements, uses 1 whole block (block 1, indices 4 to 7) and returns 34, where a plain loop would read 8 values. In "Range inside one block" the query [4, 5] uses no whole block at all: it falls inside one block and reads 2 elements.
1class SqrtBlocks {2final int B;3final int[] a, sum;4SqrtBlocks(int[] values) {5a = values.clone();6int n = a.length;7B = (int) Math.ceil(Math.sqrt(n));8sum = new int[(n + B - 1) / B];9for (int i = 0; i < n; i++) sum[i / B] += a[i];10}11long query(int l, int r) { // inclusive12long total = 0;13for (int i = l; i <= r; ) {14if (i % B == 0 && i + B - 1 <= r) { total += sum[i / B]; i += B; } // a whole block: one step15else { total += a[i]; i++; } // a partial block: element by element16}17return total;18}19}
Square root decomposition: block sums, updates and lazy block tags
Interactive · edit the inputblock 0: sum = 21
block 1: sum = 19
block 2: sum = 15
12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps.
1int B; int[] a, sum, lazy; // block b covers a[b*B .. min(n, (b+1)*B) - 1]2void build(int[] values) {3a = values; int n = a.length; B = (int) Math.ceil(Math.sqrt(n));4sum = new int[(n + B - 1) / B]; lazy = new int[sum.length];5for (int i = 0; i < n; i++) sum[i / B] += a[i];6}7void update(int i, int v) { // point update: O(1)8int cur = a[i] + lazy[i / B];9sum[i / B] += v - cur; a[i] = v - lazy[i / B];10}11int query(int l, int r) { // inclusive12int total = 0;13for (int i = l; i <= r; ) {14if (i % B == 0 && i + B - 1 <= r) { total += sum[i / B]; i += B; } // a whole block: one step15else { total += a[i] + lazy[i / B]; i++; } // a partial block: element by element16}17return total;18}19void rangeAdd(int l, int r, int v) {20for (int i = l; i <= r; ) {21int b = i / B;22if (i % B == 0 && i + B - 1 <= r && (b + 1) * B <= a.length) { lazy[b] += v; sum[b] += v * B; i += B; } // whole block: a tag23else { a[i] += v; sum[b] += v; i++; } // partial block: touch the element24}25}
Live state
n
12
block size B
4
blocks
3
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=12 block size B=4 blocks=3 | 12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps. |
| 2 | L15 | i=2 value=1 total=1 | sum[2..9]: index 2 is in a partial block: add the element 1 (total 1). |
| 3 | L15 | i=3 value=9 total=10 | sum[2..9]: index 3 is in a partial block: add the element 9 (total 10). |
| 4 | L14 | i=4 block=1 total=29 | sum[2..9]: block 1 lies fully inside [2, 9]: add its stored sum 19 in one step (total 29). |
| 5 | L15 | i=8 value=5 total=34 | sum[2..9]: index 8 is in a partial block: add the element 5 (total 34). |
| 6 | L15 | i=9 value=0 total=34 | sum[2..9]: index 9 is in a partial block: add the element 0 (total 34). |
| 7 | L17 | return=34 elements touched=4 blocks used=1 | Sum = 34, from 1 whole block and 4 single elements. A plain loop would have read 8. |
Lazy block updates
To add v to every element of a range, a plain loop costs O(length). With a lazy tag per block, a block fully inside the range gets lazy[b] += v and sum[b] += v × B in O(1), and only the two partial blocks are updated element by element. A range add then costs O(B + n / B) = O(sqrt n), whatever the length of the range.
Readers must add the tag: the true value of element i is a[i] + lazy[i / B]. Try the preset "Lazy block tags" (+2:9:3 ?0:11): the add leaves a tag on the fully covered block and touches the other elements one by one, and the following query still returns the right total, because block sums were adjusted when the tag was added.
1class LazyBlocks {2final int B, n;3final int[] a, lazy;4final long[] sum;5LazyBlocks(int[] values) {6a = values.clone();7n = a.length;8B = (int) Math.ceil(Math.sqrt(n));9sum = new long[(n + B - 1) / B];10lazy = new int[sum.length];11for (int i = 0; i < n; i++) sum[i / B] += a[i];12}13private int len(int b) { return Math.min(n, (b + 1) * B) - b * B; }14void rangeAdd(int l, int r, int v) {15for (int i = l; i <= r; ) {16int b = i / B;17if (i % B == 0 && i + len(b) - 1 <= r) { lazy[b] += v; sum[b] += (long) v * len(b); i += len(b); } // whole block: a tag18else { a[i] += v; sum[b] += v; i++; } // partial: touch the element19}20}21long query(int l, int r) {22long total = 0;23for (int i = l; i <= r; ) {24int b = i / B;25if (i % B == 0 && i + len(b) - 1 <= r) { total += sum[b]; i += len(b); }26else { total += a[i] + lazy[b]; i++; } // the tag belongs to the element too27}28return total;29}30}
Square root decomposition: block sums, updates and lazy block tags
Interactive · edit the inputblock 0: sum = 21
block 1: sum = 19
block 2: sum = 15
12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps.
1int B; int[] a, sum, lazy; // block b covers a[b*B .. min(n, (b+1)*B) - 1]2void build(int[] values) {3a = values; int n = a.length; B = (int) Math.ceil(Math.sqrt(n));4sum = new int[(n + B - 1) / B]; lazy = new int[sum.length];5for (int i = 0; i < n; i++) sum[i / B] += a[i];6}7void update(int i, int v) { // point update: O(1)8int cur = a[i] + lazy[i / B];9sum[i / B] += v - cur; a[i] = v - lazy[i / B];10}11int query(int l, int r) { // inclusive12int total = 0;13for (int i = l; i <= r; ) {14if (i % B == 0 && i + B - 1 <= r) { total += sum[i / B]; i += B; } // a whole block: one step15else { total += a[i] + lazy[i / B]; i++; } // a partial block: element by element16}17return total;18}19void rangeAdd(int l, int r, int v) {20for (int i = l; i <= r; ) {21int b = i / B;22if (i % B == 0 && i + B - 1 <= r && (b + 1) * B <= a.length) { lazy[b] += v; sum[b] += v * B; i += B; } // whole block: a tag23else { a[i] += v; sum[b] += v; i++; } // partial block: touch the element24}25}
Live state
n
12
block size B
4
blocks
3
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=12 block size B=4 blocks=3 | 12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps. |
| 2 | L15 | i=2 value=1 total=1 | sum[2..9]: index 2 is in a partial block: add the element 1 (total 1). |
| 3 | L15 | i=3 value=9 total=10 | sum[2..9]: index 3 is in a partial block: add the element 9 (total 10). |
| 4 | L14 | i=4 block=1 total=29 | sum[2..9]: block 1 lies fully inside [2, 9]: add its stored sum 19 in one step (total 29). |
| 5 | L15 | i=8 value=5 total=34 | sum[2..9]: index 8 is in a partial block: add the element 5 (total 34). |
| 6 | L15 | i=9 value=0 total=34 | sum[2..9]: index 9 is in a partial block: add the element 0 (total 34). |
| 7 | L17 | return=34 elements touched=4 blocks used=1 | Sum = 34, from 1 whole block and 4 single elements. A plain loop would have read 8. |
Rebuild trade-offs
Another way to trade updates against queries is to rebuild. Keep a prefix-sum array for fast static queries and a small list of pending changes. A query adds the pending changes that fall in the range (at most S of them); when the list reaches S, rebuild the prefix array in O(n) and clear the list. Queries cost O(S), and updates cost O(1) plus an O(n) rebuild every S updates, which is O(n / S) amortised.
Balancing the two gives S ≈ sqrt(n) again. The same idea underlies many offline and batched techniques: do cheap work often, and expensive clean-ups rarely. It also shows when decomposition is the wrong tool: with only a few updates, a plain prefix array plus brute force is simpler, and with a heavy mix a Fenwick or segment tree gives O(log n) for both.
| Structure | Query | Update | Notes |
|---|---|---|---|
| Plain array | O(n) | O(1) | no preprocessing |
| Prefix sums | O(1) | O(n) | static data |
| Square root blocks | O(sqrt n) | O(1) point, O(sqrt n) range | simple, supports many operations |
| Fenwick / segment tree | O(log n) | O(log n) | the default choice |
| Batched rebuild (S = sqrt n) | O(sqrt n) | O(sqrt n) amortised | when updates come in bursts |
1class BatchedPrefix {2private final int n, limit;3private long[] prefix; // prefix[i] = sum of the first i values at the last rebuild4private final int[] a;5private final List<int[]> pending = new ArrayList<>(); // {index, delta} since the last rebuild6BatchedPrefix(int[] values) {7a = values.clone();8n = a.length;9limit = Math.max(1, (int) Math.sqrt(n));10rebuild();11}12private void rebuild() {13prefix = new long[n + 1];14for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + a[i];15pending.clear();16}17void add(int i, int delta) {18a[i] += delta;19pending.add(new int[]{i, delta});20if (pending.size() >= limit) rebuild(); // the expensive step, once per `limit` updates21}22long sum(int l, int r) { // inclusive23long s = prefix[r + 1] - prefix[l];24for (int[] p : pending) if (p[0] >= l && p[0] <= r) s += p[1];25return s;26}27}
Square root decomposition: block sums, updates and lazy block tags
Interactive · edit the inputblock 0: sum = 21
block 1: sum = 19
block 2: sum = 15
12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps.
1int B; int[] a, sum, lazy; // block b covers a[b*B .. min(n, (b+1)*B) - 1]2void build(int[] values) {3a = values; int n = a.length; B = (int) Math.ceil(Math.sqrt(n));4sum = new int[(n + B - 1) / B]; lazy = new int[sum.length];5for (int i = 0; i < n; i++) sum[i / B] += a[i];6}7void update(int i, int v) { // point update: O(1)8int cur = a[i] + lazy[i / B];9sum[i / B] += v - cur; a[i] = v - lazy[i / B];10}11int query(int l, int r) { // inclusive12int total = 0;13for (int i = l; i <= r; ) {14if (i % B == 0 && i + B - 1 <= r) { total += sum[i / B]; i += B; } // a whole block: one step15else { total += a[i] + lazy[i / B]; i++; } // a partial block: element by element16}17return total;18}19void rangeAdd(int l, int r, int v) {20for (int i = l; i <= r; ) {21int b = i / B;22if (i % B == 0 && i + B - 1 <= r && (b + 1) * B <= a.length) { lazy[b] += v; sum[b] += v * B; i += B; } // whole block: a tag23else { a[i] += v; sum[b] += v; i++; } // partial block: touch the element24}25}
Live state
n
12
block size B
4
blocks
3
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=12 block size B=4 blocks=3 | 12 values are cut into 3 blocks of 4 (B = ceil(sqrt 12)). Each block stores its sum. A query reads whole blocks in one step and walks only the partial blocks at the two ends: at most 2 × B + n / B steps. |
| 2 | L15 | i=2 value=1 total=1 | sum[2..9]: index 2 is in a partial block: add the element 1 (total 1). |
| 3 | L15 | i=3 value=9 total=10 | sum[2..9]: index 3 is in a partial block: add the element 9 (total 10). |
| 4 | L14 | i=4 block=1 total=29 | sum[2..9]: block 1 lies fully inside [2, 9]: add its stored sum 19 in one step (total 29). |
| 5 | L15 | i=8 value=5 total=34 | sum[2..9]: index 8 is in a partial block: add the element 5 (total 34). |
| 6 | L15 | i=9 value=0 total=34 | sum[2..9]: index 9 is in a partial block: add the element 0 (total 34). |
| 7 | L17 | return=34 elements touched=4 blocks used=1 | Sum = 34, from 1 whole block and 4 single elements. A plain loop would have read 8. |
Practice ladder
Now try it yourself
Easy
- Range Sum Query - Mutable
Medium
- Range Sum Query 2D - Mutable
- Count of Smaller Numbers After Self
Advanced
- Range Frequency Queries
- Dynamic Range Sum Queries (CSES)
Key takeaways
- Blocks of about sqrt(n) with one summary each: whole blocks in one step, partial blocks element by element.
- Point updates are O(1); range updates use a lazy tag per block for O(sqrt n).
- Rebuilding every S updates trades update cost for query cost; sqrt(n) balances them.