Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Prefix Sums & Range Queries  ›  Lesson

Sqrt Decomposition

Sqrt Decomposition 30 minCut the data into √n blocks, keep one summary per block, and read whole blocks at once
You're building a piece ofQuarry — the analytics engine
This piece — block_query(): Splits the log into √n blocks so edits stay cheap and queries read about 2√n cells.
Scenario Late reports keep editing minutes Quarry has already logged, and every edit to a prefix row rewrites everything after it. Block summaries keep an edit to two writes while a dashboard query reads about 2√n cells instead of the whole range.
Your task
Build block_query(values, l, r) returning {"total": ..., "reads": ...} for minutes l..r, both included. Use blocks of b = max(1, isqrt(len(values))), where block k covers minutes k*b to k*b + b - 1 and the last block may be shorter. A block lying completely inside the range costs one read. Every other covered minute costs one read of its own. Example: block_query([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3], 2, 13) → {"total": 64, "reads": 6}.

Sqrt Decomposition

In short: split the log into blocks of about √n minutes and keep one running summary per block. An edit changes a cell and adjusts its block's summary — two writes, O(1). A range query reads whole blocks by their summary and only the two ragged ends cell by cell, for O(√n) total. Small blocks make the middle expensive and big blocks make the ragged ends expensive, and √n is the block size where neither dominates.

Twelve minutes in three blocks; a query reads three ragged cells at the left end, one whole block by its summary, then two ragged cells at the right end

Last lesson's prefix row had one weakness, and Quarry hits it straight away. Monitoring agents report late. A minute that already reads 4 errors becomes 6 when a straggler arrives, and that happens hundreds of times a minute. Every prefix cell after the edit includes the old value, so one edit rewrites up to n cells.

So there are two extremes, and each is fast at one thing:

structure edit one minute total of l..r
the raw log O(1) O(n)
the prefix row O(n) O(1)

When edits and queries are mixed, both extremes cost O(n) per operation on average. This lesson finds the middle, and it is an idea you'll use far beyond range sums: cut the data into blocks and keep one summary per block.

Blocks with summaries

Split the log into blocks of b minutes and store each block's total beside it. Minute i lives in block i // b.

  • Edit minute i: change the cell, and adjust its block's summary by the same amount. Two writes, O(1).
  • Total of l..r: the blocks lying entirely inside the range contribute their summary, one read each. Only the two blocks at the ends are partly covered, and those are read cell by cell.

Here are 16 minutes in four blocks of four, with the four summaries stored after the log. Watch a query over minutes 2..13:

watch it move · array

A block that sits wholly inside the range costs one summary read. Only the two ragged ends are read cell by cell.

The last four cells are block summaries. Block A holds 3 + 1 + 4 + 1 = 9, stored once in ΣA. 20 cells. At index 16.

1 / 9
What does this print?
n = 10
b = max(1, int(n ** 0.5))
blocks = (n + b - 1) // b
print(b, blocks)
What does this print?
b = 4
print(13 // b, 8 // b, 3 // b)

Why the square root

How big should a block be? A query reads at most b − 1 cells at each ragged end and at most n / b summaries in between, so roughly n/b + 2b reads. Small blocks make the middle expensive, and big blocks make the ends expensive. For a 400-minute log:

block size b worst-case reads
1 402
5 90
10 60
14 56.6
20 60
50 108

The bottom is near b = 14, about the square root of 200, and blocks of 13 to 15 all land around 57 reads. Both extremes waste work.

With b ≈ √n an edit is O(1) and a query is O(√n). For a month of minutes, √43,200 ≈ 208, so a query reads a few hundred cells instead of tens of thousands, and an edit is still two writes. This lesson rounds b down to math.isqrt(n). The exact constant hardly matters; the square root does.

Building it

python

Forgetting that second write is the classic bug. The log looks right and the queries are wrong:

broken — fix it

Minute 6 drops from 2 to 0, so block 3's summary should fall from 8 to 6. It stays at 8, because the change is computed after the old value is gone.

The query walks three phases: the left end cell by cell, whole blocks by summary, the right end cell by cell. When l and r fall in the same block there is no middle at all, and the loop just reads the cells. Edit the range and run it:

your code, drawn · array

Your block query. Whole blocks light up four at a time, and each costs one read.

Step through the edit on its own, and watch the summary move with the cell:

step through it
1log = [3, 1, 4, 1, 5, 9, 2, 6]
2b = 2
3sums = [4, 5, 14, 8]
4i, value = 5, 1
5change = value - log[i]
6log[i] = value
7sums[i // b] += change
8print(log, sums)

❓ Cross-question — "O(√n) is worse than O(1). Why not keep the prefix row?" Because the prefix row's O(1) query comes with an O(n) edit. For q queries and u edits mixed together, the prefix row costs O(q + u·n) and the blocks cost O((q + u)·√n). With a thousand of each on a 43,200-minute log, that is about 43 million writes against about 400,000 reads. The blocks balance the two operations, and a mixed workload needs that balance.

Beyond sums

Anything you can compute per block and combine works: minimum, maximum, counts. min is the interesting one. An edit can raise a cell that was its block's minimum, and then the old summary is wrong in a way you can't patch by adding a difference. You recompute that block from its b cells, which is still O(√n). The exercises build it.

A block can also keep a sorted copy of its cells. Then "how many values in l..r are at most x?" becomes a binary search per whole block. That's the boss exercise, and a common interview question in disguise.


Idioms & real-world patterns

Lazy tags on blocks

A range edit, "add 5 to minutes 3 to 40", would touch every cell. Instead, a block covered completely gets a pending tag: "everything here is 5 higher than stored". Its summary grows by 5 · b and its cells aren't touched. Only the ragged ends are edited cell by cell. A query adds the tag back in. That's the capstone exercise, and it is the same idea segment trees use in lesson 4, where it's called lazy propagation.

⚡ Advanced — reordering queries

If every query is known in advance, sorting them by block and walking a window across the log can answer them all in about O((n + q)·√n), even for questions no summary can answer, such as "how many distinct values in this range?". It is a whole technique on its own, and the reason √n blocks keep turning up in places that have nothing to do with sums.

Quick practice

What does this print?
from math import isqrt
n = 23
b = max(1, isqrt(n))
blocks = (n + b - 1) // b
print(b, blocks)
broken — fix it

The total 24 is right, but this reads every one of the 8 cells instead of skipping whole blocks, so it reports 8 reads instead of 4.


🎯 Your turn

Build Quarry's block summaries: block_query(values, l, r) returns {"total": ..., "reads": ...} for minutes l..r, both ends included. Use blocks of b = max(1, math.isqrt(len(values))) minutes, where block k covers minutes k·b .. k·b + b − 1 (the last block may be shorter). A block lying completely inside the range costs one read. Every other covered minute is read on its own.

  • block_query([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3], 2, 13) → {"total": 64, "reads": 6}
  • block_query([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3], 5, 6) → {"total": 11, "reads": 2}

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green means this piece of Quarry is built. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upOne total per block

Write block_summaries(values, b) returning the total of each block of b consecutive minutes, in order. The last block may be shorter. block_summaries([3, 1, 4, 1, 5, 9, 2], 3) → [8, 15, 2].

sqrt-decompositionarrays
DrillEdits and totals, mixed

Write block_ops(values, ops). Each op is ["add", i, d] (add d to minute i) or ["sum", l, r] (total of minutes l..r, both included). Keep blocks of b = max(1, isqrt(len(values))) with a summary each, so an edit is two writes. Return the answers to the sum ops, in order. block_ops([1, 2, 3, 4], [["sum", 0, 3], ["add", 2, 10], ["sum", 1, 2]]) → [10, 15].

sqrt-decompositionpoint-update
BuildThe quietest minute

Write block_min(values, ops). Each op is ["set", i, v] (minute i becomes v) or ["min", l, r] (smallest value in minutes l..r). Use blocks of b = max(1, isqrt(len(values))) with the minimum of each block as its summary. Return the answers to the min ops. block_min([5, 2, 8, 6], [["min", 0, 3], ["set", 1, 9], ["min", 0, 3]]) → [2, 5].

range-minsqrt-decomposition
BossHow many fast responses?

Interview-style. Quarry stores the response time of each request in order. Write count_at_most(times, ops) supporting two operations:

  • ["set", i, v]: request i's time is corrected to v.
  • ["count", l, r, x]: how many of requests l..r (both included) took at most x ms?

Return the answers to the count ops, in order.

Constraints: 1 <= len(times) <= 10^5, up to 10^5 ops, times between 0 and 10^9. Checking every request per query is too slow at that size. Aim for about O(√n · log n) per op. The tests are small.

Example: count_at_most([40, 10, 30, 20], [["count", 0, 3, 25], ["set", 2, 5], ["count", 1, 3, 25]]) → [2, 3].

sqrt-decompositionbisect
CapstoneRange bumps with block tags

Write block_range_add(values, ops). Each op is ["add", l, r, d] (add d to every minute in l..r) or ["sum", l, r]. Both must be O(√n): a block wholly inside an add gets a pending tag instead of having its cells touched. Use b = max(1, isqrt(len(values))). Return the sum answers. block_range_add([1, 1, 1, 1], [["add", 0, 3, 2], ["sum", 1, 2]]) → [6].

sqrt-decompositionrange-sumlazy-propagation
ExplainWhy √n is the block size

A teammate sets Quarry's block size to 10 for a 43,200-minute log, saying "small blocks are cheap to rebuild". Another suggests 5,000, saying "fewer blocks means fewer summaries". Explain what each choice costs a range query, and why a block size near √n is the one to pick.

block_query.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
block_query(values, l, r) → dictThe total of minutes l..r and how many reads the block walk made.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.