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.
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:
- Iterate over each stride length
i
from1
tom
, wherem
is the square root ofn
. - For each stride length
i
, iterate backwards through thenums
array starting from the last index. - Calculate the sum incrementally, adding
nums[j]
to the next value in the prefix already computed which issuf[i][min(n, j + i)]
. This is because the next element in the sequence we're summing would bej+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:
- Starting at index
x
, generate a range of indices by slicing thenums
array fromx
to the end of the array with stepy
. This selects everyy
th element starting atx
. - Sum the selected elements.
- 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 tom
. If it is, use the precomputedsuf
value to answer the query. If it's larger, directly calculate the sum using slicing on thenums
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.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through an example to illustrate the solution approach.
Suppose we have the following nums
array and queries:
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] queries = [[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 precomputedsuf
array.- Answer:
suf[2][1] = 1
(which is sum ofnums[1]
,nums[1+2]
,nums[1+4]
, etc., up to the end of array).
- Answer:
-
Query [0, 3]: Since the stride is 3 (which is less than or equal to
m
), we also use the precomputedsuf
array.- Answer:
suf[3][0] = 22
(which is sum ofnums[0]
,nums[0+3]
,nums[0+6]
, etc., up to the end of array).
- Answer:
-
Query [2, 1]: As the stride is 1, we refer again to the precomputed
suf
array.- Answer:
suf[1][2] = 31
(which is sum ofnums[2]
,nums[3]
,nums[4]
, ... , until the end of the array).
- Answer:
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 than10^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
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:
- When
y <= m
: The complexity for this case isO(1)
because the result is directly accessed from the precomputedsuf
array. - When
y > m
: The complexity isO(n / y)
because the sum is computed using slicing with a stepy
, so it touches everyy
th 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.
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
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
LeetCode 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 we
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!