1714. Sum Of Special Evenly-Spaced Elements In Array 🔒
Problem Description
You have an array nums
of non-negative integers (0-indexed) and a list of queries. Each query is represented as [xi, yi]
.
For each query [xi, yi]
, you need to:
- Start from index
xi
in the array - Sum up elements at positions
xi
,xi + yi
,xi + 2*yi
,xi + 3*yi
, and so on - In other words, sum all
nums[j]
wherej >= xi
and(j - xi)
is divisible byyi
The answer to each query should be returned modulo 10^9 + 7
.
For example, if nums = [1, 2, 3, 4, 5]
and query is [1, 2]
:
- Start at index 1 (value 2)
- Next valid index: 1 + 2 = 3 (value 4)
- Next valid index: 3 + 2 = 5 (out of bounds)
- Sum = 2 + 4 = 6
The solution uses block decomposition to optimize performance:
- For queries with small step sizes (≤ √n), precompute suffix sums for each starting position and step size
- For queries with large step sizes (> √n), calculate the sum directly since there won't be many elements to sum
The preprocessing creates a 2D array suf[i][j]
that stores the sum of elements starting from position j
with step size i
. This allows O(1) lookup for small step queries while keeping the overall complexity manageable.
Intuition
The naive approach would be to iterate through the array for each query, jumping by step size y
starting from index x
. This works but can be inefficient when we have many queries.
The key insight is recognizing that the efficiency depends on the step size y
:
-
Small step sizes (like 1, 2, 3): If
y
is small, we'll visit many elements. For example, with step size 1, we visit every element fromx
to the end. With step size 2, we visit half the remaining elements. These patterns repeat across different queries with the same step size. -
Large step sizes (like n/2, n-1): If
y
is large, we only visit a few elements. For example, with step sizen/2
in an array of sizen
, we visit at most 2 elements.
This observation leads to a trade-off strategy:
- For small step sizes that appear frequently and visit many elements, we should precompute the results to avoid redundant calculations
- For large step sizes that visit few elements, direct calculation is already efficient
The threshold for "small" vs "large" is chosen as √n
because:
- There are at most
√n
different small step sizes (1 to√n
) - For each small step size, we need
O(n)
space to store precomputed sums - Total preprocessing space:
O(√n * n)
which is manageable - Each large step query visits at most
n/√n = √n
elements, making direct calculationO(√n)
This balances preprocessing time/space with query efficiency, achieving O(√n)
per query regardless of step size.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements block decomposition with a threshold of √n
to distinguish between small and large step sizes.
Preprocessing Phase:
-
Calculate the threshold
m = int(sqrt(n))
to separate small and large step sizes. -
Create a 2D array
suf
with dimensions(m+1) × (n+1)
:suf[i][j]
stores the sum of elements starting from positionj
with step sizei
- We only precompute for step sizes from 1 to
m
-
Build suffix sums in reverse order:
for i in range(1, m + 1): # For each small step size for j in range(n - 1, -1, -1): # Process positions from right to left suf[i][j] = suf[i][min(n, j + i)] + nums[j]
- Processing from right to left allows us to build suffix sums incrementally
suf[i][j] = nums[j] + suf[i][j+i]
means: the sum starting atj
equals the current element plus the sum starting at the next positionj+i
min(n, j + i)
handles boundary cases whenj + i
exceeds array length
Query Processing Phase:
For each query [x, y]
:
-
If
y <= m
(small step size):- Directly return the precomputed value:
suf[y][x] % mod
- This is O(1) lookup time
- Directly return the precomputed value:
-
If
y > m
(large step size):- Calculate the sum directly using Python's slice notation:
sum(nums[x::y])
nums[x::y]
creates a slice starting at indexx
with stepy
- This visits at most
n/y < n/√n = √n
elements
- Calculate the sum directly using Python's slice notation:
Time Complexity:
- Preprocessing:
O(m × n) = O(√n × n)
- Per query:
O(1)
for small steps,O(√n)
for large steps - Total:
O(n√n + q√n)
whereq
is the number of queries
Space Complexity: O(n√n)
for storing the suffix sum array
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with a concrete example to illustrate how the block decomposition approach works.
Given:
nums = [1, 2, 3, 4, 5, 6]
(n = 6)- Queries:
[[0, 2], [1, 2], [2, 3], [0, 5]]
Step 1: Calculate threshold
m = int(sqrt(6)) = 2
- Step sizes ≤ 2 are "small", step sizes > 2 are "large"
Step 2: Build suffix sum array
We create suf[3][7]
(dimensions: (m+1) × (n+1) = 3 × 7)
For step size 1 (i=1):
- Start from right to left (j = 5 to 0)
suf[1][5] = nums[5] + suf[1][6] = 6 + 0 = 6
suf[1][4] = nums[4] + suf[1][5] = 5 + 6 = 11
suf[1][3] = nums[3] + suf[1][4] = 4 + 11 = 15
suf[1][2] = nums[2] + suf[1][3] = 3 + 15 = 18
suf[1][1] = nums[1] + suf[1][2] = 2 + 18 = 20
suf[1][0] = nums[0] + suf[1][1] = 1 + 20 = 21
For step size 2 (i=2):
suf[2][5] = nums[5] + suf[2][7] = 6 + 0 = 6
suf[2][4] = nums[4] + suf[2][6] = 5 + 0 = 5
suf[2][3] = nums[3] + suf[2][5] = 4 + 6 = 10
suf[2][2] = nums[2] + suf[2][4] = 3 + 5 = 8
suf[2][1] = nums[1] + suf[2][3] = 2 + 10 = 12
suf[2][0] = nums[0] + suf[2][2] = 1 + 8 = 9
Final suf
array:
suf[1] = [21, 20, 18, 15, 11, 6, 0] // step size 1 suf[2] = [9, 12, 8, 10, 5, 6, 0] // step size 2
Step 3: Process queries
Query 1: [0, 2]
(start at index 0, step size 2)
- Step size 2 ≤ m, so use precomputed value
- Answer:
suf[2][0] = 9
- Verification: nums[0] + nums[2] + nums[4] = 1 + 3 + 5 = 9 ✓
Query 2: [1, 2]
(start at index 1, step size 2)
- Step size 2 ≤ m, so use precomputed value
- Answer:
suf[2][1] = 12
- Verification: nums[1] + nums[3] + nums[5] = 2 + 4 + 6 = 12 ✓
Query 3: [2, 3]
(start at index 2, step size 3)
- Step size 3 > m, so calculate directly
- Visit indices: 2, 5
- Answer: nums[2] + nums[5] = 3 + 6 = 9
Query 4: [0, 5]
(start at index 0, step size 5)
- Step size 5 > m, so calculate directly
- Visit indices: 0, 5
- Answer: nums[0] + nums[5] = 1 + 6 = 7
Summary:
- Small step queries (≤2): O(1) lookup from precomputed
suf
array - Large step queries (>2): O(√n) direct calculation, visiting at most 2 elements
- All queries answered efficiently without redundant calculations
Solution Implementation
1from typing import List
2from math import sqrt
3
4class Solution:
5 def solve(self, nums: List[int], queries: List[List[int]]) -> List[int]:
6 """
7 Process queries on an array where each query asks for sum of elements
8 at arithmetic progression positions starting from index x with step y.
9
10 Args:
11 nums: Input array of integers
12 queries: List of [x, y] pairs where x is start index and y is step size
13
14 Returns:
15 List of sums for each query, modulo 10^9 + 7
16 """
17 MOD = 10**9 + 7
18 n = len(nums)
19
20 # Threshold for optimization - use precomputation for small steps
21 threshold = int(sqrt(n))
22
23 # Precompute suffix sums for small step sizes (1 to threshold)
24 # suffix_sums[step][i] stores sum of nums[i], nums[i+step], nums[i+2*step], ...
25 suffix_sums = [[0] * (n + 1) for _ in range(threshold + 1)]
26
27 for step in range(1, threshold + 1):
28 # Build suffix sums from right to left for this step size
29 for start_idx in range(n - 1, -1, -1):
30 next_idx = min(n, start_idx + step)
31 suffix_sums[step][start_idx] = suffix_sums[step][next_idx] + nums[start_idx]
32
33 result = []
34
35 # Process each query
36 for start_idx, step_size in queries:
37 if step_size <= threshold:
38 # Use precomputed suffix sum for small steps
39 result.append(suffix_sums[step_size][start_idx] % MOD)
40 else:
41 # Compute sum directly for large steps (fewer elements to sum)
42 query_sum = sum(nums[start_idx::step_size])
43 result.append(query_sum % MOD)
44
45 return result
46
1class Solution {
2 public int[] solve(int[] nums, int[][] queries) {
3 int arrayLength = nums.length;
4 int sqrtLength = (int) Math.sqrt(arrayLength);
5 final int MOD = (int) 1e9 + 7;
6
7 // Precompute suffix sums for small step sizes (1 to sqrt(n))
8 // suffixSums[step][startIndex] stores the sum of elements starting from startIndex
9 // with the given step size
10 int[][] suffixSums = new int[sqrtLength + 1][arrayLength + 1];
11
12 // Build suffix sum table for each step size from 1 to sqrt(n)
13 for (int stepSize = 1; stepSize <= sqrtLength; ++stepSize) {
14 // Process from right to left to build suffix sums
15 for (int startPos = arrayLength - 1; startPos >= 0; --startPos) {
16 // Calculate next position in the arithmetic progression
17 int nextPos = Math.min(arrayLength, startPos + stepSize);
18 // Add current element to the suffix sum from the next position
19 suffixSums[stepSize][startPos] = (suffixSums[stepSize][nextPos] + nums[startPos]) % MOD;
20 }
21 }
22
23 int queryCount = queries.length;
24 int[] result = new int[queryCount];
25
26 // Process each query
27 for (int queryIndex = 0; queryIndex < queryCount; ++queryIndex) {
28 int startIndex = queries[queryIndex][0];
29 int stepSize = queries[queryIndex][1];
30
31 if (stepSize <= sqrtLength) {
32 // For small step sizes, use precomputed suffix sums
33 result[queryIndex] = suffixSums[stepSize][startIndex];
34 } else {
35 // For large step sizes, compute the sum directly
36 // This is efficient since there will be few elements to sum
37 int sum = 0;
38 for (int position = startIndex; position < arrayLength; position += stepSize) {
39 sum = (sum + nums[position]) % MOD;
40 }
41 result[queryIndex] = sum;
42 }
43 }
44
45 return result;
46 }
47}
48
1class Solution {
2public:
3 vector<int> solve(vector<int>& nums, vector<vector<int>>& queries) {
4 int arraySize = nums.size();
5 int sqrtSize = (int) sqrt(arraySize);
6 const int MOD = 1e9 + 7;
7
8 // Precompute suffix sums for small step sizes (1 to sqrt(n))
9 // suffixSum[step][start] = sum of nums[start], nums[start + step], nums[start + 2*step], ...
10 int suffixSum[sqrtSize + 1][arraySize + 1];
11 memset(suffixSum, 0, sizeof(suffixSum));
12
13 // Build suffix sum table for each step size from 1 to sqrt(n)
14 for (int step = 1; step <= sqrtSize; ++step) {
15 // Process from right to left to build suffix sums
16 for (int startIdx = arraySize - 1; startIdx >= 0; --startIdx) {
17 int nextIdx = min(arraySize, startIdx + step);
18 suffixSum[step][startIdx] = (suffixSum[step][nextIdx] + nums[startIdx]) % MOD;
19 }
20 }
21
22 vector<int> answer;
23
24 // Process each query
25 for (auto& query : queries) {
26 int startPosition = query[0];
27 int stepSize = query[1];
28
29 if (stepSize <= sqrtSize) {
30 // For small step sizes, use precomputed suffix sum
31 answer.push_back(suffixSum[stepSize][startPosition]);
32 } else {
33 // For large step sizes, compute sum directly
34 int sum = 0;
35 for (int idx = startPosition; idx < arraySize; idx += stepSize) {
36 sum = (sum + nums[idx]) % MOD;
37 }
38 answer.push_back(sum);
39 }
40 }
41
42 return answer;
43 }
44};
45
1/**
2 * Solves range sum queries with arithmetic progression indices
3 * Uses sqrt decomposition to optimize repeated queries
4 *
5 * @param nums - Input array of numbers
6 * @param queries - Array of [startIndex, step] pairs
7 * @returns Array of sum results for each query
8 */
9function solve(nums: number[], queries: number[][]): number[] {
10 const arrayLength: number = nums.length;
11 // Square root decomposition threshold
12 const sqrtThreshold: number = Math.floor(Math.sqrt(arrayLength));
13 const MOD: number = 10 ** 9 + 7;
14
15 // Precompute suffix sums for small step values (1 to sqrtThreshold)
16 // suffixSums[step][index] stores sum starting from index with given step
17 const suffixSums: number[][] = Array(sqrtThreshold + 1)
18 .fill(0)
19 .map(() => Array(arrayLength + 1).fill(0));
20
21 // Build suffix sum table for each step size up to threshold
22 for (let step = 1; step <= sqrtThreshold; ++step) {
23 // Process from right to left to build suffix sums
24 for (let index = arrayLength - 1; index >= 0; --index) {
25 const nextIndex: number = Math.min(arrayLength, index + step);
26 suffixSums[step][index] = (suffixSums[step][nextIndex] + nums[index]) % MOD;
27 }
28 }
29
30 const results: number[] = [];
31
32 // Process each query
33 for (const [startIndex, stepSize] of queries) {
34 if (stepSize <= sqrtThreshold) {
35 // Use precomputed suffix sum for small steps
36 results.push(suffixSums[stepSize][startIndex]);
37 } else {
38 // Calculate sum directly for large steps
39 let sum: number = 0;
40 for (let i = startIndex; i < arrayLength; i += stepSize) {
41 sum = (sum + nums[i]) % MOD;
42 }
43 results.push(sum);
44 }
45 }
46
47 return results;
48}
49
Time and Space Complexity
Time Complexity: O((n + m) × √n)
The time complexity breaks down into two main parts:
-
Preprocessing phase: Building the suffix sum array
suf
- The outer loop runs for
m + 1
iterations wherem = √n
, so approximately√n
iterations - The inner loop runs for
n
iterations - Total preprocessing time:
O(√n × n) = O(n√n)
- The outer loop runs for
-
Query processing phase: Processing
m
queries (wherem
is the number of queries)- For each query with
y ≤ √n
: Direct lookup in the precomputed array takesO(1)
time - For each query with
y > √n
: Computingsum(nums[x::y])
takesO(n/y)
time. Sincey > √n
, this is at mostO(n/√n) = O(√n)
time - Total query processing time:
O(m × √n)
- For each query with
Combined time complexity: O(n√n + m√n) = O((n + m) × √n)
Space Complexity: O(n × √n)
The space complexity is determined by:
- The
suf
array has dimensions(m + 1) × (n + 1)
wherem = √n
- This gives us approximately
√n × n = n√n
space - Other variables use
O(1)
orO(n)
space which doesn't affect the overall complexity
Therefore, the space complexity is O(n√n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow in Suffix Sum Accumulation
Pitfall: When building the suffix sums, the accumulated values can become very large before applying the modulo operation. In languages with fixed integer sizes (like C++ or Java), this can cause integer overflow. Even in Python, storing unnecessarily large intermediate values wastes memory and slows computation.
Example:
# Problematic: Sums can grow extremely large before modulo suffix_sums[step][start_idx] = suffix_sums[step][next_idx] + nums[start_idx]
Solution: Apply modulo operation during the accumulation phase:
suffix_sums[step][start_idx] = (suffix_sums[step][next_idx] + nums[start_idx]) % MOD
2. Incorrect Threshold Calculation for Edge Cases
Pitfall: Using int(sqrt(n))
as the threshold can be suboptimal for small arrays. When n = 1
or n = 2
, the threshold becomes 1, which means we only precompute for step size 1, missing potential optimizations for step size 2.
Example:
# For n = 2, threshold = 1, but step size 2 could also be precomputed efficiently
threshold = int(sqrt(n))
Solution: Use a minimum threshold value or adjust based on memory constraints:
threshold = max(int(sqrt(n)), min(n, 100)) # Ensure reasonable minimum
# OR
threshold = int(sqrt(n)) if n > 4 else n
3. Memory Allocation for Very Large Arrays
Pitfall: The 2D suffix sum array uses O(n√n) memory. For very large arrays (e.g., n = 10^6), this creates a matrix of size √10^6 × 10^6 ≈ 1000 × 10^6 = 10^9 elements, which can cause memory issues.
Example:
# This can fail for large n due to memory constraints
suffix_sums = [[0] * (n + 1) for _ in range(threshold + 1)]
Solution: Implement lazy initialization or use a dictionary for sparse storage:
# Option 1: Use dictionary for actually needed combinations
suffix_sums = {}
for step in range(1, threshold + 1):
for start_idx in range(n - 1, -1, -1):
next_idx = min(n, start_idx + step)
key = (step, start_idx)
suffix_sums[key] = (suffix_sums.get((step, next_idx), 0) + nums[start_idx]) % MOD
# Option 2: Cap the threshold based on available memory
MAX_MEMORY_CELLS = 10**7 # Adjust based on constraints
threshold = min(int(sqrt(n)), MAX_MEMORY_CELLS // n)
4. Off-by-One Error in Boundary Handling
Pitfall: The boundary condition min(n, start_idx + step)
assumes that index n
in the suffix array represents "out of bounds" with value 0. If this initialization is missed or misunderstood, it can lead to incorrect sums.
Example:
# If suffix_sums[step][n] is not initialized to 0, this fails
next_idx = min(n, start_idx + step)
suffix_sums[step][start_idx] = suffix_sums[step][next_idx] + nums[start_idx]
Solution: Explicitly ensure the boundary initialization:
# Initialize boundary values explicitly
suffix_sums = [[0] * (n + 1) for _ in range(threshold + 1)]
# The (n+1)th column is already 0, which is correct for out-of-bounds
# Alternative: Handle boundary explicitly in code
for step in range(1, threshold + 1):
for start_idx in range(n - 1, -1, -1):
if start_idx + step >= n:
suffix_sums[step][start_idx] = nums[start_idx]
else:
suffix_sums[step][start_idx] = (suffix_sums[step][start_idx + step] + nums[start_idx]) % MOD
Which technique can we use to find the middle of a linked list?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!