1714. Sum Of Special Evenly-Spaced Elements In Array


Problem Description

The problem requires us to answer a set of queries on an array of non-negative integers, nums. Each query is represented by a pair [x_i, y_i], where x_i is the starting index to consider in the array and y_i is the stride for selecting elements. Specifically, for each query, we want to find the sum of every nums[j] such that j is greater than or equal to x_i and the difference (j - x_i) is divisible by y_i. Importantly, the answers to the queries should be returned modulo 10^9 + 7.

Intuition

The intuition behind the solution is based on the observation that direct computation of every query could be inefficient, especially when there are many queries or the array is very large. Thus, we need to optimize the process. For each y_i that is small (specifically, not larger than the square root of the size of nums), we can precompute a suffix sum array suf that helps answer the queries quickly. This exploits the fact that with smaller strides, there's more repetition and structure to use to our advantage.

For larger strides (y_i greater than the square root of the size of nums), it might be less efficient to use precomputation due to the sparsity of the elements we'd be summing. Therefore, for such strides, it's better to compute the sum directly per query.

This approach balances between precomputation for frequent, structured queries, and on-the-fly computation for less structured and less frequently occurring query patterns.

Learn more about Dynamic Programming patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

What is the worst case running time for finding an element in a binary search tree(not necessarily balanced) of size n?

Solution Approach

The solution approach uses a combination of precomputation and direct calculation to handle the two scenarios efficiently: smaller strides (small y_i) and larger strides (large y_i).

Precomputation for smaller strides

First, we identify the threshold for small strides, which is the square root of n, the length of nums. We prepare a 2D array suf where each row corresponds to a different stride length upto the threshold. The idea is to calculate suffix sums starting from each index j. For stride i, suf[i][j] represents the sum of elements nums[j], nums[j+i], nums[j+2i], and so on till the end of the array.

The precomputation occurs like this:

  1. Iterate over each stride length i from 1 to m, where m is the square root of n.
  2. For each stride length i, iterate backwards through the nums array starting from the last index.
  3. Calculate the sum incrementally, adding nums[j] to the next value in the prefix already computed which is suf[i][min(n, j + i)]. This is because the next element in the sequence we're summing would be j+i elements ahead considering the stride.

This process essentially fills up the suffix sum array with all the necessary sums for smaller strides.

Direct calculation for larger strides

For each query with stride y > m, the direct calculation takes place as follows:

  1. Starting at index x, generate a range of indices by slicing the nums array from x to the end of the array with step y. This selects every yth element starting at x.
  2. Sum the selected elements.
  3. Apply modulo 10^9 + 7 on the sum to avoid integer overflow and to conform with the problem requirements.

By combining these two approaches, we obtain an efficient method to answer all types of queries. The algorithm only uses precomputation for scenarios where it significantly reduces complexity, and falls back to direct summation when precomputation would not offer a speedup.

The main algorithm consists of:

  • Precomputing the suf array for smaller strides.
  • Iterating over each query.
  • Checking if the stride y of the query is smaller or equal to m. If it is, use the precomputed suf value to answer the query. If it's larger, directly calculate the sum using slicing on the nums array.
  • Each answer is then appended to the ans list, after applying the modulo operation.
  • Finally, return the ans list as the result.

This hybrid approach is particularly efficient for handling a mix of query types on potentially large datasets, as it minimizes unnecessary computation while making use of precomputation wherever beneficial.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?

Example Walkthrough

Let's walk through an example to illustrate the solution approach.

Suppose we have the following nums array and queries:

1nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
2queries = [[1, 2], [0, 3], [2, 1]]

Let's consider m to be the threshold for small strides, which is the square root of the length of nums. Here, n = 11, so m = sqrt(11) ≈ 3. Thus, any stride y_i <= 3 will involve precomputation.

Initial Setup

Precompute the suffix sum array suf for the strides less than or equal to m.

For stride 1: suf[1][...] = [35, 32, 31, 30, 29, 24, 22, 20, 14, 8, 5]

For stride 2: suf[2][...] = [36, 1, 25, 4, 21, 9, 11, 6, 8, 3, 5]

For stride 3: suf[3][...] = [22, 1, 6, 1, 14, 9, 2, 6, 5, 3, 5]

We now have precomputed sums for all smaller strides.

Answering Queries

  • Query [1, 2]: Since the stride is 2 (which is less than or equal to m), we use the precomputed suf array.

    • Answer: suf[2][1] = 1 (which is sum of nums[1], nums[1+2], nums[1+4], etc., up to the end of array).
  • Query [0, 3]: Since the stride is 3 (which is less than or equal to m), we also use the precomputed suf array.

    • Answer: suf[3][0] = 22 (which is sum of nums[0], nums[0+3], nums[0+6], etc., up to the end of array).
  • Query [2, 1]: As the stride is 1, we refer again to the precomputed suf array.

    • Answer: suf[1][2] = 31 (which is sum of nums[2], nums[3], nums[4], ... , until the end of the array).

Finally, for each answer, we apply the modulo 10^9 + 7 operation.

  • Final Answer List: [1 mod (10^9 + 7), 22 mod (10^9 + 7), 31 mod (10^9 + 7)] or [1, 22, 31] since all values are already less than 10^9 + 7.

This completes the example, which demonstrates how the algorithm efficiently answers queries by using a combination of precomputation for smaller strides and direct computation for larger ones.

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        MOD = 10**9 + 7  # Define the modulus for result as per problem statement to avoid large integers
7        n = len(nums)  # The total number of elements in nums
8        sqrt_n = int(sqrt(n))  # The square root of the length of nums, which determines the threshold
9        prefix_sums = [[0] * (n + 1) for _ in range(sqrt_n + 1)]  # Initialize the prefix sums matrix
10
11        # Fill the prefix sums matrix for all blocks with size up to sqrt(n)
12        for block_size in range(1, sqrt_n + 1):
13            for i in range(n - 1, -1, -1):  # Start from the end to compute prefix sums
14                next_index = min(n, i + block_size)
15                prefix_sums[block_size][i] = prefix_sums[block_size][next_index] + nums[i]
16
17        results = []  # This will store the results of each query
18        for start, interval in queries:
19            # If the interval is under the square root threshold, use precomputed sums
20            if interval <= sqrt_n:
21                result = prefix_sums[interval][start] % MOD
22            else:
23                # For intervals larger than square root of n, calculate the sum on the fly
24                result = sum(nums[start::interval]) % MOD
25            results.append(result)
26
27        return results  # Return the results list for all queries
28
1class Solution {
2    public int[] solve(int[] nums, int[][] queries) {
3        // Calculate the length of the nums array and the square root of that length
4        int numLength = nums.length;
5        int squareRootOfNumLength = (int) Math.sqrt(numLength);
6      
7        // Define the modulo value to avoid overflow
8        final int mod = (int) 1e9 + 7;
9      
10        // Create a 2D array for storing suffix sums
11        int[][] suffixSums = new int[squareRootOfNumLength + 1][numLength + 1];
12      
13        // Calculate suffix sums for blocks with size up to the square root of numLength
14        for (int i = 1; i <= squareRootOfNumLength; ++i) {
15            for (int j = numLength - 1; j >= 0; --j) {
16                suffixSums[i][j] = (suffixSums[i][Math.min(numLength, j + i)] + nums[j]) % mod;
17            }
18        }
19      
20        // Get the number of queries and initialize an array to store the answers
21        int queryCount = queries.length;
22        int[] answers = new int[queryCount];
23      
24        // Process each query
25        for (int i = 0; i < queryCount; ++i) {
26            int startIndex = queries[i][0];
27            int stepSize = queries[i][1];
28          
29            // If the step size is within the computed suffix sums, use the precomputed value
30            if (stepSize <= squareRootOfNumLength) {
31                answers[i] = suffixSums[stepSize][startIndex];
32            } else {
33                // If the step size is larger, calculate the sum on the fly
34                int sum = 0;
35                for (int j = startIndex; j < numLength; j += stepSize) {
36                    sum = (sum + nums[j]) % mod;
37                }
38                answers[i] = sum;
39            }
40        }
41      
42        // Return the array of answers
43        return answers;
44    }
45}
46
1#include <vector>
2#include <cstring>
3#include <cmath>
4
5class Solution {
6public:
7    std::vector<int> solve(std::vector<int>& nums, std::vector<std::vector<int>>& queries) {
8        int numsSize = nums.size();
9        int blockSize = static_cast<int>(sqrt(numsSize)); // The size of each block for the sqrt decomposition.
10        const int mod = 1e9 + 7; // The modulo value to prevent integer overflow.
11        int suffix[blockSize + 1][numsSize + 1]; // Suffix sums matrix.
12      
13        // Initialize the suffix sums matrix with zeros.
14        memset(suffix, 0, sizeof(suffix));
15
16        // Pre-compute the suffix sums for all possible blocks.
17        for (int i = 1; i <= blockSize; ++i) {
18            for (int j = numsSize - 1; j >= 0; --j) {
19                suffix[i][j] = (suffix[i][std::min(numsSize, j + i)] + nums[j]) % mod;
20            }
21        }
22
23        std::vector<int> ans; // Vector to store the answers to the queries.
24
25        // Iterate over each query and calculate the sum accordingly.
26        for (auto& query : queries) {
27            int start = query[0], step = query[1]; // Start index and step for the current query.
28
29            // If the step is less than or equal to the block size, use the precomputed suffix sums.
30            if (step <= blockSize) {
31                ans.push_back(suffix[step][start]);
32            } else {
33                // Otherwise, perform a brute-force sum calculation.
34                int sum = 0;
35                for (int i = start; i < numsSize; i += step) {
36                    sum = (sum + nums[i]) % mod;
37                }
38                ans.push_back(sum); // Add the computed sum to the answers vector.
39            }
40        }
41
42        return ans; // Return the vector of answers.
43    }
44};
45
1// Defining a function to solve the query based on the array of numbers and list of queries
2function solve(nums: number[], queries: number[][]): number[] {
3    const arrayLength: number = nums.length;                   // Length of the nums array
4    const sqrtLength: number = Math.floor(Math.sqrt(arrayLength)); // Sqrt decomposition length
5    const modulus: number = 1e9 + 7;                          // Define modulus for large numbers
6
7    // Suffix arrays to store pre-computed sums. These are used to answer queries efficiently for small y values
8    const suffixSums: number[][] = Array(sqrtLength + 1)      
9        .fill(0)
10        .map(() => Array(arrayLength + 1).fill(0));
11
12    // Pre-compute the suffix sums for each possible block size up to the square root of the length of the array
13    for (let blockSize = 1; blockSize <= sqrtLength; ++blockSize) {
14        for (let startIndex = arrayLength - 1; startIndex >= 0; --startIndex) {
15            let nextIndex = Math.min(arrayLength, startIndex + blockSize);
16            suffixSums[blockSize][startIndex] = (suffixSums[blockSize][nextIndex] + nums[startIndex]) % modulus;
17        }
18    }
19
20    // Array to hold answer for each query
21    const answers: number[] = [];
22
23    // Process each query
24    for (const [startIndex, stepSize] of queries) {
25        if (stepSize <= sqrtLength) {
26            // If stepSize is within the pre-computed range, use pre-computed sum for efficiency
27            answers.push(suffixSums[stepSize][startIndex]);
28        } else {
29            // For larger step sizes, calculate the sum manually
30            let sum = 0;
31            for (let i = startIndex; i < arrayLength; i += stepSize) {
32                sum = (sum + nums[i]) % modulus;
33            }
34            answers.push(sum); // Append the sum to the answers array
35        }
36    }
37
38    return answers; // Return the array containing sums for each query
39}
40
Not Sure What to Study? Take the 2-min Quiz:

Which of the following uses divide and conquer strategy?

Time and Space Complexity

Time Complexity

The time complexity of the precomputation step (building the suf array) is O(m * n), where m is int(sqrt(n)) and n is the length of the input array nums. This is because the outer loop runs m times and the inner loop runs n times.

For each query in queries, there are two cases to consider:

  1. When y <= m: The complexity for this case is O(1) because the result is directly accessed from the precomputed suf array.
  2. When y > m: The complexity is O(n / y) because the sum is computed using slicing with a step y, so it touches every yth element.

Given that there are q queries, if we denote the number of queries where y > m as q1 and where y <= m as q2, then the total time for all queries is O(q1 * (n / y) + q2). However, in the worst case, all queries could be such that y > m, which makes it O(q * (n / y)).

The total time complexity is therefore O(m * n + q * (n / y)). In the worst case scenario where y is just above m, this could be approximated as O(m * n + q * n / m), which simplifies to O(m * n + q * sqrt(n)).

Space Complexity

The space complexity is primarily due to the additional 2D list suf. Since suf has a size of (m+1) * (n+1), its space complexity is O(m * n). Additionally, the space used by ans to store the results grows linearly with the number of queries q, hence O(q). Therefore, the total space complexity is O(m * n + q).

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫