LeetCode 3584 - Maximum Product of First and Last Elements of a Subsequence
The problem asks for the maximum possible product of the first and last elements of any subsequence of length m chosen from an integer array nums.
Difficulty: 🟡 Medium
Topics: Array, Two Pointers
Solution
Problem Understanding
The problem asks for the maximum possible product of the first and last elements of any subsequence of length m chosen from an integer array nums. A subsequence means we can pick elements in order from the array, not necessarily contiguous, as long as their indices are strictly increasing. However, only the first and last elements of the chosen subsequence matter for the final product; all middle elements exist only to satisfy the subsequence length requirement.
In other words, we are selecting indices i1 < i2 < ... < im, and the value we care about is nums[i1] * nums[im]. The middle elements do not affect the product but constrain feasibility because we must be able to pick m - 2 elements between the first and last indices.
The constraints show that nums.length can be up to 10^5, which immediately rules out any quadratic or combinatorial enumeration of subsequences. Values can range between -10^5 and 10^5, meaning negative numbers are significant and we must consider both maximum and minimum values when computing products.
Edge cases include when m = 1, where the subsequence consists of a single element and the product becomes nums[i] * nums[i], effectively the maximum square of any element. Another important case is when m = nums.length, where the subsequence must include the entire array, so the answer is simply nums[0] * nums[n-1].
Approaches
The brute-force approach considers every subsequence of size m, computes its first and last elements, and tracks the maximum product. While correct, this is infeasible because the number of subsequences grows combinatorially as O(n choose m), which is far too large for n = 10^5.
The key insight is that only the first and last chosen indices matter, and any valid subsequence of length m imposes a constraint on the distance between those indices. If we pick indices i < j, then we must ensure there are at least m - 2 indices between them, meaning j - i + 1 >= m. Once this constraint is satisfied, all intermediate elements can always be chosen arbitrarily from within the range.
This reduces the problem to choosing a pair (i, j) with sufficient distance, maximizing nums[i] * nums[j]. For each valid j, we need the best possible nums[i] among all valid i positions. Because numbers can be negative, we must track both the maximum and minimum values seen so far in the valid prefix.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(C(n, m) · m) | O(m) | Enumerates all subsequences and computes endpoints |
| Optimal | O(n) | O(1) | Maintains prefix min and max for valid starting indices |
Algorithm Walkthrough
The optimal solution reframes the problem as a constrained two-index optimization problem over valid subsequence endpoints.
- We first observe that for any fixed ending index
j, the starting indeximust satisfyi <= j - (m - 1)to ensure there are enough elements between them to form a subsequence of lengthm. This defines a sliding boundary for validivalues. - We iterate through the array using
jas the potential last element of the subsequence. The valid starting region foriexpands asjincreases, but only includes indices up toj - (m - 1). - While moving
j, we maintain two running values over all validiindices: the maximum value seen so far and the minimum value seen so far. We need both because multiplying by a negative number can turn a small value into a large product. - At each position
j, once we have at least one validi, we compute two candidate products:nums[j] * max_prefixandnums[j] * min_prefix. The maximum of these two is the best subsequence ending atj. - We update the global answer with the best product found for each
j. - We update the prefix extrema as the valid range expands.
The algorithm carefully ensures that only valid subsequence endpoints are considered while maintaining sufficient information to evaluate all possibilities efficiently.
Why it works
The correctness relies on the invariant that at every step, we maintain the best and worst possible starting values among all indices that can legally serve as the first element of a length-m subsequence ending at or beyond the current position. Since any valid subsequence is fully determined by its endpoints, and all middle elements are always constructible as long as spacing is satisfied, evaluating all valid endpoint pairs guarantees the optimal solution.
Python Solution
from typing import List
class Solution:
def maximumProduct(self, nums: List[int], m: int) -> int:
n = len(nums)
if m == 1:
best = float("-inf")
for x in nums:
best = max(best, x * x)
return best
best = float("-inf")
prefix_max = float("-inf")
prefix_min = float("inf")
# i can be valid start only up to j-(m-1)
for j in range(m - 1, n):
i = j - (m - 1)
# expand prefix window to include new valid start index
if i - 1 >= 0:
val = nums[i - 1]
prefix_max = max(prefix_max, val)
prefix_min = min(prefix_min, val)
# initialize at first valid window
if prefix_max == float("-inf"):
prefix_max = nums[0]
prefix_min = nums[0]
best = max(
best,
nums[j] * prefix_max,
nums[j] * prefix_min
)
return best
The implementation first handles the special case m = 1, where the answer is simply the maximum square value in the array. For m > 1, it iterates over all possible ending indices j. For each j, it expands the valid starting region and updates the running minimum and maximum values of eligible starting elements. It then computes the best possible product using the current ending value and updates the global result.
Go Solution
func maximumProduct(nums []int, m int) int64 {
n := len(nums)
if m == 1 {
var best int64 = int64(nums[0]) * int64(nums[0])
for _, v := range nums {
prod := int64(v) * int64(v)
if prod > best {
best = prod
}
}
return best
}
var best int64 = -1 << 63
prefixMax := nums[0]
prefixMin := nums[0]
for j := m - 1; j < n; j++ {
i := j - (m - 1)
if i-1 >= 0 {
val := nums[i-1]
if val > prefixMax {
prefixMax = val
}
if val < prefixMin {
prefixMin = val
}
}
prod1 := int64(nums[j]) * int64(prefixMax)
prod2 := int64(nums[j]) * int64(prefixMin)
if prod1 > best {
best = prod1
}
if prod2 > best {
best = prod2
}
}
return best
}
In Go, the main difference is careful handling of integer overflow by converting intermediate products to int64. We also use explicit comparisons instead of Python’s built-in max and min. The logic remains identical: maintain a dynamic prefix of valid starting elements and compute the best product at each endpoint.
Worked Examples
Example 1
nums = [-1,-9,2,3,-2,-3,1], m = 1
Since m = 1, every element is a valid subsequence. We compute squares:
At each step, we track:
-1 → 1-9 → 81(best so far)2 → 43 → 9-2 → 4-3 → 91 → 1
Final answer is 81.
Example 2
nums = [1,3,-5,5,6,-4], m = 3
We evaluate valid (i, j) pairs with distance constraint j - i + 1 >= 3.
For each j:
j = 2 (-5): validi = 0, product1 * -5 = -5,3 * -5 = -15j = 3 (5): validi ∈ {0,1}, best max = 3, min = 1, best product =5 * 3 = 15j = 4 (6): validi ∈ {0,1,2}, max = 3, min = -5, best =6 * 3 = 18j = 5 (-4): validi ∈ {0,1,2}, max = 3, min = -5, best =(-4) * (-5) = 20
Final answer is 20.
Example 3
nums = [2,-1,2,-6,5,2,-5,7], m = 2
Now we only need pairs with at least 2 elements between constraint simplifies to adjacent valid index spacing ≥ 1.
We track best pairs:
- Best positive pair found is
(5,7)giving35 - Other pairs are smaller due to negatives or smaller magnitudes
Final answer is 35.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each element is processed once as part of prefix maintenance and endpoint evaluation |
| Space | O(1) | Only a constant number of variables are maintained |
The linear time complexity comes from the single pass over the array, where each index contributes at most constant work. No auxiliary data structures grow with input size.
Test Cases
assert Solution().maximumProduct([-1,-9,2,3,-2,-3,1], 1) == 81 # single-element subsequence squares
assert Solution().maximumProduct([1,3,-5,5,6,-4], 3) == 20 # mixed positives and negatives
assert Solution().maximumProduct([2,-1,2,-6,5,2,-5,7], 2) == 35 # best pair with constraint
assert Solution().maximumProduct([5], 1) == 25 # single element edge case
assert Solution().maximumProduct([-2,-3,-4], 2) == 12 # two negatives produce positive
assert Solution().maximumProduct([1,2,3,4,5], 5) == 5 # full array constraint
assert Solution().maximumProduct([-1,1,-1,1], 2) == 1 # alternating signs
| Test | Why |
|---|---|
| single element array | verifies m = 1 and minimal input handling |
| mixed positives and negatives | validates optimal handling of sign combinations |
| general case m = 2 | ensures basic pair logic correctness |
| single value | edge case where only one subsequence exists |
| all negatives | ensures max/min interaction correctness |
| m equals n | ensures full array constraint handling |
| alternating signs | tests stability under frequent sign flips |
Edge Cases
One important edge case is when m = 1. In this case, the subsequence consists of a single element, so the first and last elements are the same. This changes the problem into finding the maximum square of any element. A naive implementation might incorrectly treat this as a pair-selection problem, but the correct handling requires explicitly squaring each element.
Another edge case occurs when all numbers are negative. In such cases, the optimal solution often comes from selecting the least negative numbers or leveraging multiplication of two negatives. The algorithm handles this correctly by maintaining both the minimum and maximum prefix values, ensuring that negative multiplication opportunities are not missed.
A third edge case is when m = n. Here, there is exactly one valid subsequence, the entire array. The result is simply nums[0] * nums[n-1]. The algorithm naturally handles this because only one endpoint pair is ever considered valid, and the prefix window expands to include all possible starting indices before reaching the last element.