Square Root Decomposition
1. Range Query Block Division
| Concept | Description | Complexity | Use Case |
|---|---|---|---|
| Block Size | Divide array of size n into √n blocks, each of size √n | O(√n) |
Balance between query and update time |
| Block Storage | Store precomputed values for each block (sum, min, max, etc.) | Space: O(√n) |
Fast block-level queries |
| Range Query | Combine full blocks + partial blocks at boundaries | O(√n) |
Sum/min/max over range [L, R] |
| Point Update | Update single element + update its block's precomputed value | O(1) |
Modify single array element |
Example: Range sum query with updates
class SqrtDecomposition {
constructor(arr) {
this.arr = [...arr];
this.n = arr.length;
this.blockSize = Math.floor(Math.sqrt(this.n));
this.numBlocks = Math.ceil(this.n / this.blockSize);
this.blocks = new Array(this.numBlocks).fill(0);
// Build block sums
for (let i = 0; i < this.n; i++) {
const blockIdx = Math.floor(i / this.blockSize);
this.blocks[blockIdx] += arr[i];
}
}
// Update value at index
update(idx, val) {
const blockIdx = Math.floor(idx / this.blockSize);
this.blocks[blockIdx] += val - this.arr[idx];
this.arr[idx] = val;
}
// Query sum in range [left, right]
query(left, right) {
let sum = 0;
const leftBlock = Math.floor(left / this.blockSize);
const rightBlock = Math.floor(right / this.blockSize);
if (leftBlock === rightBlock) {
// Both in same block - iterate elements
for (let i = left; i <= right; i++) {
sum += this.arr[i];
}
} else {
// Left partial block
const leftEnd = (leftBlock + 1) * this.blockSize - 1;
for (let i = left; i <= leftEnd; i++) {
sum += this.arr[i];
}
// Full blocks in middle
for (let b = leftBlock + 1; b < rightBlock; b++) {
sum += this.blocks[b];
}
// Right partial block
const rightStart = rightBlock * this.blockSize;
for (let i = rightStart; i <= right; i++) {
sum += this.arr[i];
}
}
return sum;
}
}
// Example usage
const arr = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
const sqd = new SqrtDecomposition(arr);
console.log(sqd.query(2, 7)); // Sum of [5,7,9,11,13,15] = 60
sqd.update(5, 20); // Change 11 to 20
console.log(sqd.query(2, 7)); // Sum now = 69
2. Mo's Algorithm Query Ordering
| Concept | Strategy | Complexity | Application |
|---|---|---|---|
| Query Reordering | Process queries in special order to minimize pointer movements | O((n + q)√n) |
Offline query processing |
| Block Sorting | Sort queries by (left/√n, right) - primary by left block, secondary by right | Sort: O(q log q) |
Group queries by left block |
| Add/Remove | Maintain current range [L, R], expand/contract to match each query | Per query: O(√n) |
Incrementally update answer |
| State Tracking | Keep frequency map or other structure for current range | Space: O(n) |
Track distinct elements, frequencies, etc. |
Example: Mo's Algorithm - Distinct elements in ranges
function mosAlgorithm(arr, queries) {
const n = arr.length;
const q = queries.length;
const blockSize = Math.floor(Math.sqrt(n));
// Add index to each query and sort
const indexedQueries = queries.map((query, idx) => ({
left: query[0],
right: query[1],
idx: idx
}));
// Sort queries by block of left, then by right
indexedQueries.sort((a, b) => {
const blockA = Math.floor(a.left / blockSize);
const blockB = Math.floor(b.left / blockSize);
if (blockA !== blockB) return blockA - blockB;
return a.right - b.right;
});
const result = new Array(q);
const freq = new Map();
let distinctCount = 0;
let currLeft = 0, currRight = -1;
// Helper functions
function add(idx) {
const val = arr[idx];
const count = freq.get(val) || 0;
if (count === 0) distinctCount++;
freq.set(val, count + 1);
}
function remove(idx) {
const val = arr[idx];
const count = freq.get(val);
if (count === 1) distinctCount--;
freq.set(val, count - 1);
}
// Process queries in sorted order
for (const query of indexedQueries) {
const {left, right, idx} = query;
// Expand/contract current range to match query range
while (currRight < right) {
currRight++;
add(currRight);
}
while (currRight > right) {
remove(currRight);
currRight--;
}
while (currLeft < left) {
remove(currLeft);
currLeft++;
}
while (currLeft > left) {
currLeft--;
add(currLeft);
}
result[idx] = distinctCount;
}
return result;
}
// Example
const arr = [1, 1, 2, 1, 3, 4, 5, 2, 8];
const queries = [[0, 4], [0, 8], [2, 5]];
console.log(mosAlgorithm(arr, queries));
// [3, 6, 4] - distinct elements in each range
Note: Mo's algorithm is offline - all queries must be known beforehand. The key insight: sorting queries by blocks minimizes L/R pointer movements from O(n×q) to O((n+q)√n).
3. Update Query Trade-off
| Structure | Query Time | Update Time | Best For |
|---|---|---|---|
| Plain Array | O(n) range query |
O(1) update |
Many updates, few queries |
| Prefix Sum | O(1) range query |
O(n) rebuild |
Static data, many queries |
| Sqrt Decomposition | O(√n) range query |
O(1) update |
Balanced queries and updates |
| Segment Tree | O(log n) query |
O(log n) update |
Best overall, more complex |
| Fenwick Tree (BIT) | O(log n) query |
O(log n) update |
Simple implementation, prefix queries |
Example: Range minimum with updates
class SqrtMinQuery {
constructor(arr) {
this.arr = [...arr];
this.n = arr.length;
this.blockSize = Math.floor(Math.sqrt(this.n));
this.numBlocks = Math.ceil(this.n / this.blockSize);
this.blockMin = new Array(this.numBlocks).fill(Infinity);
this.buildBlocks();
}
buildBlocks() {
for (let i = 0; i < this.n; i++) {
const blockIdx = Math.floor(i / this.blockSize);
this.blockMin[blockIdx] = Math.min(
this.blockMin[blockIdx],
this.arr[i]
);
}
}
update(idx, val) {
this.arr[idx] = val;
// Rebuild affected block
const blockIdx = Math.floor(idx / this.blockSize);
const start = blockIdx * this.blockSize;
const end = Math.min(start + this.blockSize, this.n);
this.blockMin[blockIdx] = Infinity;
for (let i = start; i < end; i++) {
this.blockMin[blockIdx] = Math.min(
this.blockMin[blockIdx],
this.arr[i]
);
}
}
queryMin(left, right) {
let minVal = Infinity;
const leftBlock = Math.floor(left / this.blockSize);
const rightBlock = Math.floor(right / this.blockSize);
if (leftBlock === rightBlock) {
for (let i = left; i <= right; i++) {
minVal = Math.min(minVal, this.arr[i]);
}
} else {
// Left partial
const leftEnd = (leftBlock + 1) * this.blockSize - 1;
for (let i = left; i <= leftEnd; i++) {
minVal = Math.min(minVal, this.arr[i]);
}
// Middle full blocks
for (let b = leftBlock + 1; b < rightBlock; b++) {
minVal = Math.min(minVal, this.blockMin[b]);
}
// Right partial
const rightStart = rightBlock * this.blockSize;
for (let i = rightStart; i <= right; i++) {
minVal = Math.min(minVal, this.arr[i]);
}
}
return minVal;
}
}
const arr = [5, 2, 8, 1, 9, 3, 7, 4, 6];
const smq = new SqrtMinQuery(arr);
console.log(smq.queryMin(2, 6)); // 1
smq.update(3, 10);
console.log(smq.queryMin(2, 6)); // 3
Complexity Comparison
| Operation | Sqrt Decomp | Segment Tree |
|---|---|---|
| Build | O(n) |
O(n) |
| Query | O(√n) |
O(log n) |
| Update | O(1) or O(√n) |
O(log n) |
| Space | O(n + √n) |
O(2n) |
| Implementation | Simple | More complex |
Note: Choose sqrt decomposition when:
- Implementation simplicity matters
- √n performance is acceptable
- Segment tree seems overkill
4. Sqrt Decomposition on Arrays
| Application | Block Storage | Query Operation | Example Problem |
|---|---|---|---|
| Range Sum | Sum of each block | Sum full blocks + partial blocks at ends | Sum of elements in range [L, R] |
| Range Min/Max | Min/Max of each block | Min/Max across blocks + check partial blocks | Minimum element in range |
| Range GCD | GCD of each block | GCD of full blocks + partial blocks | GCD of range elements |
| Distinct Count | Frequency map per block | Merge frequencies from relevant blocks | Count distinct in range |
| Range Update | Lazy propagation flag per block | Apply lazy updates when querying | Add value to all elements in range |
Example: Range GCD queries
function gcd(a, b) {
while (b !== 0) {
[a, b] = [b, a % b];
}
return a;
}
class RangeGCD {
constructor(arr) {
this.arr = [...arr];
this.n = arr.length;
this.blockSize = Math.floor(Math.sqrt(this.n));
this.numBlocks = Math.ceil(this.n / this.blockSize);
this.blockGCD = new Array(this.numBlocks).fill(0);
// Build block GCDs
for (let i = 0; i < this.n; i++) {
const blockIdx = Math.floor(i / this.blockSize);
this.blockGCD[blockIdx] = gcd(this.blockGCD[blockIdx], arr[i]);
}
}
query(left, right) {
let result = 0;
const leftBlock = Math.floor(left / this.blockSize);
const rightBlock = Math.floor(right / this.blockSize);
if (leftBlock === rightBlock) {
// Same block
for (let i = left; i <= right; i++) {
result = gcd(result, this.arr[i]);
}
} else {
// Left partial block
const leftEnd = (leftBlock + 1) * this.blockSize - 1;
for (let i = left; i <= leftEnd; i++) {
result = gcd(result, this.arr[i]);
}
// Full blocks
for (let b = leftBlock + 1; b < rightBlock; b++) {
result = gcd(result, this.blockGCD[b]);
}
// Right partial block
const rightStart = rightBlock * this.blockSize;
for (let i = rightStart; i <= right; i++) {
result = gcd(result, this.arr[i]);
}
}
return result;
}
}
const arr = [2, 4, 6, 8, 10, 12, 14, 16];
const rgcd = new RangeGCD(arr);
console.log(rgcd.query(0, 3)); // gcd(2,4,6,8) = 2
console.log(rgcd.query(2, 5)); // gcd(6,8,10,12) = 2
5. Block-wise Processing Pattern
| Pattern | Technique | Benefit | Use Case |
|---|---|---|---|
| Batch Processing | Group operations into blocks, process entire blocks together | Cache efficiency, reduced overhead | Matrix operations, image processing |
| Lazy Propagation | Mark blocks as modified, apply changes only when needed | Defer expensive operations | Range update queries |
| Block Rebuilding | When block updates exceed threshold, rebuild entire block | Amortized efficiency | Dynamic data with occasional updates |
| Sqrt Partitioning | Divide problems into √n groups for balanced complexity | Optimal space-time trade-off | Query optimization problems |
Example: Range add with lazy propagation
class RangeAddQuery {
constructor(arr) {
this.arr = [...arr];
this.n = arr.length;
this.blockSize = Math.floor(Math.sqrt(this.n));
this.numBlocks = Math.ceil(this.n / this.blockSize);
this.lazy = new Array(this.numBlocks).fill(0); // Lazy values
}
// Add value to range [left, right]
rangeAdd(left, right, val) {
const leftBlock = Math.floor(left / this.blockSize);
const rightBlock = Math.floor(right / this.blockSize);
if (leftBlock === rightBlock) {
// Same block - update individually
for (let i = left; i <= right; i++) {
this.arr[i] += val;
}
} else {
// Left partial block
const leftEnd = (leftBlock + 1) * this.blockSize - 1;
for (let i = left; i <= leftEnd; i++) {
this.arr[i] += val;
}
// Full blocks - use lazy propagation
for (let b = leftBlock + 1; b < rightBlock; b++) {
this.lazy[b] += val;
}
// Right partial block
const rightStart = rightBlock * this.blockSize;
for (let i = rightStart; i <= right; i++) {
this.arr[i] += val;
}
}
}
// Get value at index
get(idx) {
const blockIdx = Math.floor(idx / this.blockSize);
return this.arr[idx] + this.lazy[blockIdx];
}
// Query sum in range
rangeSum(left, right) {
let sum = 0;
for (let i = left; i <= right; i++) {
sum += this.get(i);
}
return sum;
}
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const raq = new RangeAddQuery(arr);
raq.rangeAdd(2, 6, 10); // Add 10 to indices 2-6
console.log(raq.get(4)); // 15 (was 5)
console.log(raq.rangeSum(0, 8)); // Sum after update
Example: Mo's Algorithm with modifications
// Mo's Algorithm pattern with custom comparator
function mosWithCustomSort(arr, queries, blockSize = null) {
const n = arr.length;
const size = blockSize || Math.floor(Math.sqrt(n));
// Create indexed queries
const indexedQueries = queries.map((q, idx) => ({
left: q[0],
right: q[1],
idx: idx,
block: Math.floor(q[0] / size)
}));
// Hilbert curve ordering for better cache performance
indexedQueries.sort((a, b) => {
if (a.block !== b.block) {
return a.block - b.block;
}
// Alternate direction based on block parity
return a.block % 2 === 0 ?
a.right - b.right :
b.right - a.right;
});
return indexedQueries;
}
// Benefits of alternating direction:
// - Reduces pointer movement between queries
// - Better cache locality
// - Can reduce total operations by ~25%
console.log("Block size optimization:");
console.log("For n = 10000, optimal block size ≈",
Math.floor(Math.sqrt(10000))); // 100
console.log("Expected operations per query:",
Math.sqrt(10000) + Math.sqrt(10000)); // ~200
Warning: Square root decomposition has limitations:
- Not optimal: O(√n) is slower than O(log n) segment trees for large n
- Fixed block size: Once chosen, changing block size requires rebuilding
- Mo's requires offline: Cannot handle online queries
- Memory access: Can have poor cache performance for very large arrays
Summary: Square Root Decomposition Pattern
- Core Idea: Divide array into √n blocks of size √n each, precompute values for each block
- Block Size Choice: √n provides optimal balance - fewer blocks = larger partials, more blocks = more block storage
- Range Queries: Process O(√n) full blocks + O(√n) partial elements at boundaries = O(√n) total
- Point Updates: Update element O(1) + update block value O(1) or O(√n) depending on operation
- Mo's Algorithm: Offline query optimization - sort queries by (left/√n, right), process in order with O((n+q)√n) total
- Query Ordering: Hilbert curve or alternating direction improves cache performance
- Lazy Propagation: Mark blocks with pending updates, apply when queried - defers expensive operations
- Trade-offs: Simpler than segment trees but slower; better than brute force; good middle ground
- Applications: Range sum/min/max/GCD queries, distinct counts, frequency queries, problem-specific operations
- When to Use: Need range queries + updates, segment tree too complex, O(√n) performance acceptable (n ≤ 10^5)