DSA DSA In Depth
0%
design sqrt-decompositionmos-algorithmoffline-queries

√N Block Decomposition & Mo's Offline Query Ordering (O((N+Q)√N))

7 min read Module 3.3: Range Query & Sublinear Decomposition Structures Updated Oct 23, 2026
📄 Cheat Sheet
Square Root Decomposition & Mo's Offline Query Ordering Algorithm
Errichto Hard Algorithms Square Root Decomposition & Mo's Offline Query Ordering Algorithm Watch on YouTube ↗

Walks through block sizing B = sqrt(N) and how sorting offline queries by (L / B, R) bounds total pointer movement to O((N + Q) sqrt(N)).

0:00 / --:--

1. Online $\sqrt{N}$ Block Decomposition vs Offline Mo's Algorithm

There are two primary ways to apply square-root decomposition to array range queries:

  • Online $\sqrt{N}$ Block Decomposition: Partition $A[0..N-1]$ into $\lceil N / B \rceil$ blocks of size $B \approx \sqrt{N}$. Each block maintains a summary (or sorted copy / frequency bucket). A range query $[L, R]$ inspects at most $\lceil N / B \rceil$ full blocks in the interior plus at most $2B$ individual elements in the two partial boundary blocks ($\lfloor L/B \rfloor$ and $\lfloor R/B \rfloor$), taking $O(B + N/B) = O(\sqrt{N})$ time.
  • Offline Mo's Algorithm: Reorders $Q$ static queries $[L_i, R_i]$ so a single sliding window $[\text{curL}, \text{curR}]$ visits all queries with minimal total Manhattan movement $\sum (|L_i - L_{i-1}| + |R_i - R_{i-1}|)$.
Optimal Block Size & Snake-Order Amortized Bound: For $N$ elements and $Q$ queries, let block size be $B = \max\!\left(1, \left\lfloor \frac{N}{\sqrt{Q}} \right\rfloor\right)$.
  • Left Pointer Movement: Within the same block ($\lfloor L_i / B \rfloor = \lfloor L_{i-1} / B \rfloor$), $|L_i - L_{i-1}| < B$. Across all $Q$ queries (including block transitions), $L$ moves at most $Q \cdot B + N = O(N \sqrt{Q})$ steps.
  • Right Pointer Movement: Within each of the $N / B$ blocks, $R$ is sorted monotonically so it moves at most $N$ steps per block. Across all $N / B$ blocks, $R$ moves at most $(N / B) \cdot N = N^2 / B = O(N \sqrt{Q})$ steps. Using snake order (ascending $R$ on even blocks, descending $R$ on odd blocks) eliminates the wasted $O(N)$ rewind of $R$ between adjacent blocks, cutting total $R$ movement in half!
Mo's Algorithm in the (L, R) Query Plane: Snake Ordering Across L-Blocks Left Endpoint L (Grouped into Blocks of width B = N / sqrt(Q)) R Block 0 (R asc ↑) Block 1 (R desc ↓) Block 2 (R asc ↑) Block 3 (R desc ↓)

2. Critical Implementation Rule: Expand Window Before Contracting!

When transitioning the active window $[\text{curL}, \text{curR}]$ to target $[L, R]$, the order of the four while loops is critical! If you execute remove(curL++) or remove(curR--) before expanding add(--curL) and add(++curR), and the new query $[L, R]$ is disjoint from $[\text{curL}, \text{curR}]$, the intermediate window transiently becomes inverted ($\text{curL} > \text{curR} + 1$) with negative element counts! Always run the two expansion loops (--curL, ++curR) first, and the two contraction loops (curL++, curR--) second.

flowchart LR S["Start Query [L, R]"] --> E1["1. while (curL > L) add(--curL)"] E1 --> E2["2. while (curR < R) add(++curR)"] E2 --> C1["3. while (curL < L) remove(curL++)"] C1 --> C2["4. while (curR > R) remove(curR--)"] C2 --> Ans["Record Answer for [L, R]"]
Hardware & Cache Reality — Hilbert Curve Ordering vs Block Snake Ordering: While block-snake ordering guarantees $O(N \sqrt{Q})$ Manhattan movement, mapping each query point $(L, R)$ to its 1D index along a space-filling Hilbert Curve of order $2^k \ge N$ and sorting queries by hilbertOrder(L, R) eliminates the block-size parameter $B$ completely and improves cache locality on skewed query distributions by another 20%–35%.
Heavy-Light Decomposition, Cartesian Tree Stack & Mo’s Snake-Order Sweep
O((N+Q)√N) Mo · O(log² N) HLD · O(N) Cartesian

Switch between three foundational range-query reductions: (1) Mo’s offline √N block-snake query ordering, (2) Heavy-Light path decomposition into ≤ 2 log₂ N contiguous Segment Tree intervals, and (3) O(N) Monotonic Stack Cartesian Tree construction.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Step 1 (Even Block 0): Queries with L ∈ Block 0 are sorted by R ascending. curR sweeps right monotonically (6 → 7) in O(N) total steps for the block.
🌳 Live 2D Hierarchical Tree Topology — Heavy-Light Decomposition, Cartesian Tree Stack & Mo’s Snake-Order Sweep Visited/Active Pivot/Root Resolved
Even Blo Q1:[1… [0] curR adv Q2:[0… [1] Q3:[3… [2] Q4:[4… [3]
[0]Q1:[1, 6] (blk 0, R↑)Even Block 0: Sweep R Ascending (6 → 7)
[1]Q2:[0, 7] (blk 0, R↑)curR advances +1
[2]Q3:[3, 5] (blk 1, R↓)
[3]Q4:[4, 2] (blk 1, R↓)
Optimal Block Size B
B = max(1, ⌊N / √Q⌋)
Left Pointer Moves
≤ Q · B = O(N√Q)
Right Pointer Moves
≤ (N/B) · N = O(N√Q)
[Step 1] In Even Block 0, curR moves right from 6 to 7 (+1 step) while curL jitters within Block 0 by at most B = √N steps.

3. Production Implementation: Online $\sqrt{N}$ Decomposition & Offline Mo's Algorithm

/**
 * 1. Online Sqrt Block Decomposition for Range Sum + Point Update
 * Block size B = ceil(sqrt(N)) | Point Update: O(1) | Range Query: O(sqrt(N))
 */
class SqrtBlockDecomposition {
  constructor(arr) {
    this.n = arr.length;
    this.B = Math.max(1, Math.ceil(Math.sqrt(this.n)));
    this.arr = Float64Array.from(arr);
    this.blocks = new Float64Array(Math.ceil(this.n / this.B));
    for (let i = 0; i < this.n; i++) {
      this.blocks[Math.floor(i / this.B)] += this.arr[i];
    }
  }

  update(idx, newVal) {
    const b = Math.floor(idx / this.B);
    this.blocks[b] += newVal - this.arr[idx];
    this.arr[idx] = newVal;
  }

  query(l, r) {
    let sum = 0;
    const bL = Math.floor(l / this.B);
    const bR = Math.floor(r / this.B);
    if (bL === bR) {
      for (let i = l; i <= r; i++) sum += this.arr[i];
      return sum;
    }
    for (let i = l, end = (bL + 1) * this.B; i < end; i++) sum += this.arr[i];
    for (let b = bL + 1; b < bR; b++) sum += this.blocks[b];
    for (let i = bR * this.B; i <= r; i++) sum += this.arr[i];
    return sum;
  }
}

/**
 * 2. Offline Mo's Algorithm with Boustrophedon (Snake) Ordering
 * Example: Count distinct values in each query [l, r] in O((N + Q) sqrt(N))
 */
function mosDistinctQueries(arr, queries) {
  const n = arr.length;
  const q = queries.length;
  const B = Math.max(1, Math.floor(n / Math.sqrt(Math.max(1, q))));

  // Coordinate compress arr into [0 .. M-1] so freq[] fits in a fast Int32Array
  const unique = Array.from(new Set(arr)).sort((a, b) => a - b);
  const rankMap = new Map(unique.map((v, idx) => [v, idx]));
  const a = Int32Array.from(arr, (v) => rankMap.get(v));

  const indexedQueries = queries.map(([l, r], idx) => ({
    l,
    r,
    idx,
    block: Math.floor(l / B)
  }));

  // Snake order sort: primary by L-block, secondary by R (asc on even block, desc on odd block)
  indexedQueries.sort((q1, q2) => {
    if (q1.block !== q2.block) return q1.block - q2.block;
    return (q1.block & 1) ? (q2.r - q1.r) : (q1.r - q2.r);
  });

  const freq = new Int32Array(unique.length);
  const answers = new Int32Array(q);
  let distinct = 0;
  let curL = 0, curR = -1;

  for (let i = 0; i < q; i++) {
    const { l, r, idx } = indexedQueries[i];
    // ALWAYS expand (--curL, ++curR) BEFORE contracting (curL++, curR--)!
    while (curL > l) {
      const val = a[--curL];
      if (++freq[val] === 1) distinct++;
    }
    while (curR < r) {
      const val = a[++curR];
      if (++freq[val] === 1) distinct++;
    }
    while (curL < l) {
      const val = a[curL++];
      if (--freq[val] === 0) distinct--;
    }
    while (curR > r) {
      const val = a[curR--];
      if (--freq[val] === 0) distinct--;
    }
    answers[idx] = distinct;
  }
  return answers;
}

4. Complexity Matrix

Algorithm Variant Optimal Block Size $B$ Time Complexity Supports Point Updates?
Online $\sqrt{N}$ Block Decomposition $B = \lceil \sqrt{N} \rceil$ $O(1)$ update, $O(\sqrt{N})$ query Yes (online)
Standard Mo's Algorithm (Snake / Hilbert) $B = \lfloor N / \sqrt{Q} \rfloor$ $O(N \sqrt{Q} + Q \log Q)$ No (static offline)
Mo's Algorithm with Updates (3D $(L, R, T)$) $B = N^{2/3}$ $O(Q \cdot N^{2/3})$ Yes (offline time dimension $T$)
Rollback Mo's Algorithm (Delete-Free Monotonic Ops) $B = \lfloor N / \sqrt{Q} \rfloor$ $O((N + Q)\sqrt{N})$ No (handles max/DSU without delete)
Common Pitfall — Using $B = \sqrt{N}$ When $Q \gg N$ or $Q \ll N$, and Contracting Before Expanding: First, if $N = 10^5$ and $Q = 4 \times 10^5$, setting $B = \sqrt{N} = 316$ instead of $N / \sqrt{Q} = 158$ doubles the left-pointer movement cost. Second, never put the curL < l or curR > r removal loops before the expansion loops: if curL crosses past curR + 1, the window temporarily contains negative counts (freq[v] = -1), corrupting frequency-bucket invariants or causing out-of-bounds array accesses.
Check yourself Question 1 of 4
Concept Check Optimal Block Size B When N and Q Differ recall

In Mo's Algorithm over an array of length $N$ with $Q$ offline queries, left-pointer movement costs $O(Q \cdot B)$ and right-pointer movement costs $O((N / B) \cdot N) = O(N^2 / B)$, where $B$ is the block width.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

How do we maintain the exact maximum frequency `maxFreq` in **$O(1)$ worst-case time per `add` AND `remove` step**?

Key Takeaways

Square-root block decomposition partitions an array of size N into blocks of size B = sqrt(N), answering range queries in O(sqrt(N)) and point updates in O(1).
Mo's algorithm reorders offline queries [L, R] by (floor(L / B), R with snake parity) where B = N / sqrt(Q), bounding total window movement to O(N sqrt(Q)).
Always execute the two window expansion loops (--curL, ++curR) before the two window contraction loops (curL++, curR--) so the window never inverts.
When removal is harder than insertion (e.g., maintaining maximum frequency or DSU connected components), Rollback Mo's resets the left pointer to the block boundary after each query.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue