2941. Maximum GCD-Sum of a Subarray 🔒
Problem Description
You are given an array of integers nums
and an integer k
.
For any array a
, the gcd-sum is calculated by:
- Finding the sum
s
of all elements ina
- Finding the greatest common divisor
g
of all elements ina
- Computing the gcd-sum as
s * g
Your task is to find the maximum gcd-sum among all possible subarrays of nums
that contain at least k
elements.
For example, if you have a subarray [6, 9, 12]
with k = 2
:
- Sum
s = 6 + 9 + 12 = 27
- GCD
g = gcd(6, 9, 12) = 3
- gcd-sum =
27 * 3 = 81
You need to check all valid subarrays (those with length ≥ k
) and return the maximum gcd-sum found.
The solution uses a sliding window approach combined with GCD tracking. It maintains a list f
that stores pairs of (starting_index, gcd_value)
for all subarrays ending at the current position. For each position i
, it:
- Updates the GCD values for all previous subarrays by including
nums[i]
- Removes duplicate GCD values to optimize performance
- Checks all subarrays ending at position
i
with length ≥k
and updates the maximum gcd-sum
The prefix sum array s
is used to quickly calculate subarray sums, where s[i+1] - s[j]
gives the sum of elements from index j
to i
.
Intuition
The key observation is that as we extend a subarray by adding more elements, the GCD can only stay the same or decrease (it never increases). This is because the GCD of a set of numbers is always less than or equal to the GCD of any subset.
Consider what happens when we fix the ending position of subarrays. For all subarrays ending at position i
, we need to track their GCDs. However, there's an important property: the number of distinct GCD values for all subarrays ending at a fixed position is limited.
Why? Because each time we extend a subarray to the left (making it longer), the GCD either stays the same or becomes a divisor of the previous GCD. Since a number has a limited number of divisors (at most O(log n)
), we won't have too many distinct GCD values.
This leads us to the approach: for each ending position i
, we maintain a list of (starting_position, gcd_value)
pairs. But we only keep one representative for each distinct GCD value - specifically, the rightmost starting position that gives that GCD. This is optimal because a shorter subarray with the same GCD will always have a smaller or equal sum compared to a longer one.
As we move to the next position i+1
, we update all existing GCDs by computing gcd(existing_gcd, nums[i+1])
. If multiple subarrays now have the same GCD after this update, we only keep the shortest one (rightmost starting position).
The prefix sum array allows us to calculate any subarray sum in O(1)
time. For each valid subarray (length ≥ k
), we compute its gcd-sum and track the maximum.
This approach efficiently explores all possible subarrays while avoiding redundant calculations by leveraging the monotonic property of GCD values.
Learn more about Math and Binary Search patterns.
Solution Approach
The implementation uses a sliding window technique with GCD tracking. Let's walk through the algorithm step by step:
1. Initialize Prefix Sum Array:
s = list(accumulate(nums, initial=0))
We create a prefix sum array where s[i]
represents the sum of elements from index 0 to i-1. This allows us to calculate any subarray sum in O(1) time using s[right+1] - s[left]
.
2. Main Loop - Process Each Ending Position:
For each position i
in the array, we treat it as the ending position of potential subarrays:
for i, v in enumerate(nums):
3. Update GCD Values for Existing Subarrays:
g = [] for j, x in f: y = gcd(x, v) if not g or g[-1][1] != y: g.append((j, y))
- For each subarray
(j, x)
in our listf
(wherej
is starting index andx
is GCD), we compute the new GCD when includingnums[i]
- We only add
(j, y)
to the new listg
if this GCD valuey
is different from the last one we added - This deduplication is crucial: if multiple subarrays have the same GCD, we keep only the one with the largest starting index (shortest subarray)
4. Add Current Element as New Subarray:
f = g f.append((i, v))
After updating existing subarrays, we add a new subarray starting and ending at position i
with GCD = nums[i]
.
5. Calculate Maximum GCD-Sum:
for j, x in f:
if i - j + 1 >= k:
ans = max(ans, (s[i + 1] - s[j]) * x)
For each subarray ending at position i
:
- Check if its length
(i - j + 1)
is at leastk
- If valid, calculate its gcd-sum:
sum * gcd = (s[i+1] - s[j]) * x
- Update the maximum answer
Time Complexity: O(n² × log(max_value)) where n is the array length. The log factor comes from GCD computation.
Space Complexity: O(n) for storing the prefix sums and the list of (index, gcd) pairs.
The algorithm efficiently handles the problem by maintaining only distinct GCD values for each ending position, avoiding redundant calculations while ensuring we find the maximum gcd-sum.
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 algorithm with nums = [6, 3, 9]
and k = 2
.
Initial Setup:
- Prefix sum array:
s = [0, 6, 9, 18]
- Answer:
ans = 0
- GCD tracking list:
f = []
Iteration 1 (i=0, v=6):
- Update existing subarrays:
f
is empty, sog = []
- Add new subarray starting at index 0:
f = [(0, 6)]
- Check valid subarrays (length ≥ 2): None yet (current length is 1)
ans = 0
Iteration 2 (i=1, v=3):
- Update existing subarrays:
- For
(0, 6)
: new GCD =gcd(6, 3) = 3
g = [(0, 3)]
- For
- Add new subarray starting at index 1:
f = [(0, 3), (1, 3)]
- Check valid subarrays:
- Subarray
[0,1]
: length = 2 ≥ k- Sum =
s[2] - s[0] = 9 - 0 = 9
- GCD = 3
- gcd-sum =
9 × 3 = 27
- Sum =
- Subarray
[1,1]
: length = 1 < k (skip)
- Subarray
ans = 27
Iteration 3 (i=2, v=9):
- Update existing subarrays:
- For
(0, 3)
: new GCD =gcd(3, 9) = 3
- For
(1, 3)
: new GCD =gcd(3, 9) = 3
- Since both have GCD = 3, keep only the rightmost:
g = [(1, 3)]
- For
- Add new subarray starting at index 2:
f = [(1, 3), (2, 9)]
- Check valid subarrays:
- Subarray
[1,2]
: length = 2 ≥ k- Sum =
s[3] - s[1] = 18 - 6 = 12
- GCD = 3
- gcd-sum =
12 × 3 = 36
- Sum =
- Subarray
[2,2]
: length = 1 < k (skip)
- Subarray
ans = max(27, 36) = 36
Final Result: The maximum gcd-sum is 36 from subarray [3, 9]
.
Note how the algorithm efficiently handled duplicate GCD values in iteration 3. Both subarrays [6,3,9]
and [3,9]
have GCD = 3, but we only kept the shorter one [3,9]
since it would have a smaller sum. This optimization reduces redundant calculations while ensuring we find the maximum gcd-sum.
Solution Implementation
1from typing import List
2from itertools import accumulate
3from math import gcd
4
5
6class Solution:
7 def maxGcdSum(self, nums: List[int], k: int) -> int:
8 # Compute prefix sums for quick subarray sum calculation
9 # prefix_sums[i] = sum of nums[0:i]
10 prefix_sums = list(accumulate(nums, initial=0))
11
12 # Store pairs of (starting_index, gcd_value) for subarrays ending at current position
13 # This list maintains unique GCD values to avoid redundant calculations
14 gcd_segments = []
15
16 # Track the maximum product of subarray sum and GCD
17 max_product = 0
18
19 # Process each element as a potential subarray endpoint
20 for current_index, current_value in enumerate(nums):
21 # Build new GCD segments by extending previous segments with current element
22 new_gcd_segments = []
23
24 for start_index, prev_gcd in gcd_segments:
25 # Calculate GCD when extending the subarray to include current element
26 new_gcd = gcd(prev_gcd, current_value)
27
28 # Only keep segments with unique GCD values (optimization)
29 # If the last segment has the same GCD, we don't need to store duplicates
30 if not new_gcd_segments or new_gcd_segments[-1][1] != new_gcd:
31 new_gcd_segments.append((start_index, new_gcd))
32
33 # Update segments for next iteration
34 gcd_segments = new_gcd_segments
35
36 # Add a new segment starting at current position
37 gcd_segments.append((current_index, current_value))
38
39 # Check all valid subarrays ending at current position
40 for start_index, subarray_gcd in gcd_segments:
41 subarray_length = current_index - start_index + 1
42
43 # Only consider subarrays with length at least k
44 if subarray_length >= k:
45 # Calculate sum of subarray using prefix sums
46 subarray_sum = prefix_sums[current_index + 1] - prefix_sums[start_index]
47
48 # Calculate product and update maximum
49 product = subarray_sum * subarray_gcd
50 max_product = max(max_product, product)
51
52 return max_product
53
1class Solution {
2 /**
3 * Finds the maximum value of (sum of subarray) * (GCD of subarray)
4 * where subarray has at least k elements
5 *
6 * @param nums the input array
7 * @param k minimum size of subarray
8 * @return maximum value of sum * gcd for valid subarrays
9 */
10 public long maxGcdSum(int[] nums, int k) {
11 int n = nums.length;
12
13 // Build prefix sum array for quick range sum calculation
14 // prefixSum[i] represents sum of first i elements
15 long[] prefixSum = new long[n + 1];
16 for (int i = 1; i <= n; ++i) {
17 prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
18 }
19
20 // List to store pairs of [starting index, GCD value] for subarrays ending at current position
21 List<int[]> gcdSegments = new ArrayList<>();
22 long maxResult = 0;
23
24 // Iterate through each position as potential end of subarray
25 for (int i = 0; i < n; ++i) {
26 // Build new GCD segments for position i
27 List<int[]> newGcdSegments = new ArrayList<>();
28
29 // Update existing GCD segments by including nums[i]
30 for (int[] segment : gcdSegments) {
31 int startIndex = segment[0];
32 int previousGcd = segment[1];
33
34 // Calculate new GCD when extending the segment to include nums[i]
35 int newGcd = gcd(previousGcd, nums[i]);
36
37 // Only add if this creates a new unique GCD value
38 // (merge consecutive segments with same GCD)
39 if (newGcdSegments.isEmpty() ||
40 newGcdSegments.get(newGcdSegments.size() - 1)[1] != newGcd) {
41 newGcdSegments.add(new int[] {startIndex, newGcd});
42 }
43 }
44
45 // Update the segments list
46 gcdSegments = newGcdSegments;
47
48 // Add new segment starting at current position
49 gcdSegments.add(new int[] {i, nums[i]});
50
51 // Check all valid subarrays ending at position i
52 for (int[] segment : gcdSegments) {
53 int startIndex = segment[0];
54 int gcdValue = segment[1];
55
56 // Check if subarray length meets minimum requirement
57 if (i - startIndex + 1 >= k) {
58 // Calculate sum of subarray from startIndex to i
59 long subarraySum = prefixSum[i + 1] - prefixSum[startIndex];
60 // Update maximum result
61 maxResult = Math.max(maxResult, subarraySum * gcdValue);
62 }
63 }
64 }
65
66 return maxResult;
67 }
68
69 /**
70 * Calculates Greatest Common Divisor using Euclidean algorithm
71 *
72 * @param a first number
73 * @param b second number
74 * @return GCD of a and b
75 */
76 private int gcd(int a, int b) {
77 return b == 0 ? a : gcd(b, a % b);
78 }
79}
80
1class Solution {
2public:
3 long long maxGcdSum(vector<int>& nums, int k) {
4 int n = nums.size();
5
6 // Build prefix sum array for quick range sum calculation
7 // prefixSum[i] = sum of nums[0...i-1]
8 vector<long long> prefixSum(n + 1, 0);
9 for (int i = 1; i <= n; ++i) {
10 prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
11 }
12
13 // Store pairs of (starting index, GCD value) for all subarrays ending at current position
14 vector<pair<int, int>> gcdRanges;
15 long long maxResult = 0;
16
17 // Iterate through each position as potential end of subarray
18 for (int i = 0; i < n; ++i) {
19 // Build new GCD ranges by extending previous ranges with current element
20 vector<pair<int, int>> newGcdRanges;
21
22 for (const auto& [startIdx, prevGcd] : gcdRanges) {
23 // Calculate new GCD when extending the range
24 int newGcd = __gcd(prevGcd, nums[i]);
25
26 // Only add if this creates a new distinct GCD value
27 // (consecutive ranges with same GCD are merged)
28 if (newGcdRanges.empty() || newGcdRanges.back().second != newGcd) {
29 newGcdRanges.emplace_back(startIdx, newGcd);
30 }
31 }
32
33 // Move the new ranges to current ranges
34 gcdRanges = move(newGcdRanges);
35
36 // Add new range starting at current position
37 gcdRanges.emplace_back(i, nums[i]);
38
39 // Check all possible subarrays ending at position i
40 for (const auto& [startIdx, gcdValue] : gcdRanges) {
41 int subarrayLength = i - startIdx + 1;
42
43 // Only consider subarrays with at least k elements
44 if (subarrayLength >= k) {
45 // Calculate sum of subarray [startIdx, i]
46 long long subarraySum = prefixSum[i + 1] - prefixSum[startIdx];
47
48 // Update maximum result
49 maxResult = max(maxResult, subarraySum * gcdValue);
50 }
51 }
52 }
53
54 return maxResult;
55 }
56};
57
1/**
2 * Finds the maximum value of (sum of subarray) * (GCD of subarray)
3 * for all subarrays of length at least k
4 * @param nums - The input array of positive integers
5 * @param k - The minimum length of subarray to consider
6 * @returns The maximum value of sum * GCD for valid subarrays
7 */
8function maxGcdSum(nums: number[], k: number): number {
9 const arrayLength: number = nums.length;
10
11 // Build prefix sum array for efficient range sum calculation
12 // prefixSum[i] represents sum of elements from index 0 to i-1
13 const prefixSum: number[] = Array(arrayLength + 1).fill(0);
14 for (let i = 1; i <= arrayLength; i++) {
15 prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
16 }
17
18 // Store pairs of [startIndex, GCD value] for subarrays ending at current position
19 let gcdIntervals: [number, number][] = [];
20 let maxResult: number = 0;
21
22 // Iterate through each position as potential end of subarray
23 for (let currentIndex = 0; currentIndex < arrayLength; ++currentIndex) {
24 // Build new GCD intervals by extending previous intervals with current element
25 const newGcdIntervals: [number, number][] = [];
26
27 // Process each previous interval
28 for (const [startIndex, previousGcd] of gcdIntervals) {
29 // Calculate new GCD when including current element
30 const newGcd: number = gcd(previousGcd, nums[currentIndex]);
31
32 // Only add if this creates a new GCD value (avoid duplicates)
33 if (newGcdIntervals.length === 0 || newGcdIntervals.at(-1)[1] !== newGcd) {
34 newGcdIntervals.push([startIndex, newGcd]);
35 }
36 }
37
38 // Update intervals for next iteration
39 gcdIntervals = newGcdIntervals;
40
41 // Add new interval starting at current position
42 gcdIntervals.push([currentIndex, nums[currentIndex]]);
43
44 // Check all valid subarrays ending at current position
45 for (const [startIndex, currentGcd] of gcdIntervals) {
46 const subarrayLength: number = currentIndex - startIndex + 1;
47
48 // Only consider subarrays with length at least k
49 if (subarrayLength >= k) {
50 // Calculate sum using prefix sum array
51 const subarraySum: number = prefixSum[currentIndex + 1] - prefixSum[startIndex];
52 const product: number = subarraySum * currentGcd;
53 maxResult = Math.max(maxResult, product);
54 }
55 }
56 }
57
58 return maxResult;
59}
60
61/**
62 * Calculates the Greatest Common Divisor (GCD) of two numbers using Euclidean algorithm
63 * @param a - First number
64 * @param b - Second number
65 * @returns The GCD of a and b
66 */
67function gcd(a: number, b: number): number {
68 return b === 0 ? a : gcd(b, a % b);
69}
70
Time and Space Complexity
Time Complexity: O(n² × log(max(nums)))
The algorithm iterates through each element in nums
(n iterations). For each element at position i:
- It processes the list
f
which can contain at mostO(n)
entries in the worst case - For each entry in
f
, it computes the GCD with the current element, which takesO(log(max(nums)))
time - The deduplication step ensures that
f
contains at mostO(log(max(nums)))
distinct GCD values after processing, as the GCD can only decrease and has at mostO(log(max(nums)))
distinct divisors - However, in the worst case scenario before deduplication, we process up to
i
elements
Therefore, the total time complexity is O(n × n × log(max(nums)))
= O(n² × log(max(nums)))
.
Space Complexity: O(n)
The space usage consists of:
- The prefix sum array
s
which usesO(n)
space - The list
f
which stores at mostO(min(n, log(max(nums))))
entries after deduplication, but this is bounded byO(n)
- The temporary list
g
which also uses at mostO(n)
space during construction - Other variables use
O(1)
space
The dominant space usage is O(n)
from the prefix sum array and the working lists.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling GCD Deduplication Correctly
The Pitfall: A common mistake is either forgetting to deduplicate GCD values or implementing it incorrectly. Without deduplication, the algorithm stores redundant segments with the same GCD value, leading to:
- Exponential growth in the number of segments stored
- Time Limit Exceeded (TLE) for large inputs
- Unnecessary repeated calculations
Incorrect Implementation:
# WRONG: Storing all segments without deduplication for start_index, prev_gcd in gcd_segments: new_gcd = gcd(prev_gcd, current_value) new_gcd_segments.append((start_index, new_gcd)) # Always appending
Why This Fails:
Consider the array [12, 12, 12, 12, 12]
. Without deduplication:
- Position 0: segments = [(0, 12)]
- Position 1: segments = [(0, 12), (1, 12)]
- Position 2: segments = [(0, 12), (1, 12), (2, 12)]
- Position 3: segments = [(0, 12), (1, 12), (2, 12), (3, 12)]
- Position 4: segments = [(0, 12), (1, 12), (2, 12), (3, 12), (4, 12)]
All these segments have the same GCD (12), but we're storing them all unnecessarily.
Correct Solution:
for start_index, prev_gcd in gcd_segments: new_gcd = gcd(prev_gcd, current_value) # Only append if this GCD is different from the last one added if not new_gcd_segments or new_gcd_segments[-1][1] != new_gcd: new_gcd_segments.append((start_index, new_gcd))
2. Incorrect Prefix Sum Usage
The Pitfall:
Misunderstanding how prefix sums work, especially with the initial=0
parameter, leading to off-by-one errors.
Incorrect Implementation:
# WRONG: Incorrect indexing
prefix_sums = list(accumulate(nums)) # No initial=0
subarray_sum = prefix_sums[current_index] - prefix_sums[start_index]
Why This Fails:
Without initial=0
, prefix_sums[i]
contains the sum up to and including nums[i]
. This makes it impossible to calculate the sum starting from index 0 correctly.
Correct Solution:
# With initial=0, prefix_sums[i] = sum of nums[0:i]
prefix_sums = list(accumulate(nums, initial=0))
# Sum from start_index to current_index (inclusive)
subarray_sum = prefix_sums[current_index + 1] - prefix_sums[start_index]
3. Forgetting to Add Single-Element Segments
The Pitfall: Only updating existing segments with the current element but forgetting to create a new segment starting at the current position.
Incorrect Implementation:
# WRONG: Missing the single-element segment for start_index, prev_gcd in gcd_segments: new_gcd = gcd(prev_gcd, current_value) if not new_gcd_segments or new_gcd_segments[-1][1] != new_gcd: new_gcd_segments.append((start_index, new_gcd)) gcd_segments = new_gcd_segments # Forgot to add: gcd_segments.append((current_index, current_value))
Why This Fails:
This misses all subarrays that start at position current_index
. For example, if k=1
and the maximum gcd-sum comes from a single element, this implementation would miss it entirely.
Correct Solution:
# After processing existing segments, add new segment for current position gcd_segments = new_gcd_segments gcd_segments.append((current_index, current_value))
4. Integer Overflow in Other Languages
The Pitfall:
When implementing in languages like C++ or Java, the product subarray_sum * subarray_gcd
might overflow 32-bit integers.
Example:
- Array with large values:
[1000000, 1000000, ..., 1000000]
(1000 elements) - Sum = 10^9, GCD = 10^6
- Product = 10^15 (exceeds 32-bit integer range)
Solution: Use 64-bit integers (long long in C++, long in Java) for storing sums and products:
// C++ example long long subarray_sum = prefix_sums[current_index + 1] - prefix_sums[start_index]; long long product = subarray_sum * (long long)subarray_gcd;
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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
Want a Structured Path to Master System Design Too? Don’t Miss This!