Facebook Pixel

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:

  1. Start from index xi in the array
  2. Sum up elements at positions xi, xi + yi, xi + 2*yi, xi + 3*yi, and so on
  3. In other words, sum all nums[j] where j >= xi and (j - xi) is divisible by yi

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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 from x to the end. With step size 2, we visit half the remaining elements. These patterns repeat across different queries with the same step size.

  2. Large step sizes (like n/2, n-1): If y is large, we only visit a few elements. For example, with step size n/2 in an array of size n, 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 calculation O(√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:

  1. Calculate the threshold m = int(sqrt(n)) to separate small and large step sizes.

  2. Create a 2D array suf with dimensions (m+1) × (n+1):

    • suf[i][j] stores the sum of elements starting from position j with step size i
    • We only precompute for step sizes from 1 to m
  3. 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 at j equals the current element plus the sum starting at the next position j+i
    • min(n, j + i) handles boundary cases when j + i exceeds array length

Query Processing Phase:

For each query [x, y]:

  1. If y <= m (small step size):

    • Directly return the precomputed value: suf[y][x] % mod
    • This is O(1) lookup time
  2. 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 index x with step y
    • This visits at most n/y < n/√n = √n elements

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) where q 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 Evaluator

Example 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:

  1. Preprocessing phase: Building the suffix sum array suf

    • The outer loop runs for m + 1 iterations where m = √n, so approximately √n iterations
    • The inner loop runs for n iterations
    • Total preprocessing time: O(√n × n) = O(n√n)
  2. Query processing phase: Processing m queries (where m is the number of queries)

    • For each query with y ≤ √n: Direct lookup in the precomputed array takes O(1) time
    • For each query with y > √n: Computing sum(nums[x::y]) takes O(n/y) time. Since y > √n, this is at most O(n/√n) = O(√n) time
    • Total query processing time: O(m × √n)

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) where m = √n
  • This gives us approximately √n × n = n√n space
  • Other variables use O(1) or O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More