Maximum Product of First and Last Elements of a Subsequence
MedPrompt
You are given an integer array nums and an integer m.
Return the maximum product of the first and last elements of any subsequence of nums of size m.
Example 1:
Input: nums = [-1,-9,2,3,-2,-3,1], m = 1
Output: 81
Explanation:
The subsequence [-9] has the largest product of the first and last elements: -9 * -9 = 81. Therefore, the answer is 81.
Example 2:
Input: nums = [1,3,-5,5,6,-4], m = 3
Output: 20
Explanation:
The subsequence [-5, 6, -4] has the largest product of the first and last elements.
Example 3:
Input: nums = [2,-1,2,-6,5,2,-5,7], m = 2
Output: 35
Explanation:
The subsequence [5, 7] has the largest product of the first and last elements.
Constraints:
1 <= nums.length <= 105-105 <= nums[i] <= 1051 <= m <= nums.length
Approaches
3 approaches with complexity analysis and trade-offs.
The most optimal solution achieves linear time complexity without using any extra space (O(1) space). We can combine the precomputation and the main calculation into a single pass. We use a two-pointer-like approach. One pointer j iterates from m-1 to the end of the array, representing the last element of the subsequence. Another pointer i_ptr tracks the end of the prefix from which the first element can be chosen. As j moves forward, i_ptr also moves forward, and we maintain the running minimum and maximum of the prefix ending at i_ptr. This way, we always have the necessary information to calculate the best product for each j without storing entire prefix arrays.
Algorithm
This approach builds upon the logic of the prefix array method but eliminates the need for extra space by computing the prefix information on-the-fly.
- Initialize
maxProductto a very small value. - Initialize
prefixMaxandprefixMintonums[0]. These will track the running min/max of the prefix relevant to the first elementnums[i]. - Initialize a pointer,
i_ptr, to0. This pointer marks the end of the prefix for whichprefixMaxandprefixMinhave been calculated. - Iterate with a pointer
jfromm-1ton-1. Thisjrepresents the index of the last element. - For each
j, the maximum possible index for the first elementiisi_limit = j - m + 1. - We need the min/max of the prefix
nums[0...i_limit]. Since ouri_ptrmight be behindi_limit, we advancei_ptrup toi_limit, updatingprefixMaxandprefixMinwith each new element encountered. - Once
i_ptrequalsi_limit,prefixMaxandprefixMinhold the required values. - Calculate the candidate product using
nums[j]with bothprefixMaxandprefixMin. - Update the global
maxProductwith the larger of these candidates. - After the loop over
jfinishes,maxProductholds the result.
Walkthrough
This approach refines the prefix array method to use constant extra space. Instead of pre-calculating and storing all prefix minimums and maximums in an array, we can calculate them as needed during a single pass.
We iterate through the possible last element indices j from m-1 to n-1. For each j, the first element nums[i] can be chosen from the prefix nums[0...j-m+1]. We can maintain the minimum and maximum of this prefix in two variables. As j increments, the prefix nums[0...j-m+1] grows by one element. We can update our running prefix min/max in O(1) amortized time. This is a form of a sliding window or two-pointer technique where one pointer j scans the array, and another pointer i_ptr defines the boundary of the prefix being considered.
This single-pass algorithm is optimal in both time and space.
class Solution { public long maximumProduct(int[] nums, int m) { int n = nums.length; // This logic correctly handles m=1, as j-i >= 0 means i<=j, and // max_{i<=j}(nums[i]*nums[j]) is equivalent to max_k(nums[k]*nums[k]). if (n < m) { return 0; // Or throw an exception for invalid input } long maxProduct = Long.MIN_VALUE; long prefixMax = nums[0]; long prefixMin = nums[0]; int i_ptr = 0; // Iterate j as the index of the last element for (int j = m - 1; j < n; j++) { // The first element's index `i` can be at most `j - m + 1` int i_limit = j - m + 1; // Update prefix min/max to cover the range up to i_limit // This inner loop runs disjointly over the course of the outer loop while (i_ptr < i_limit) { i_ptr++; prefixMax = Math.max(prefixMax, nums[i_ptr]); prefixMin = Math.min(prefixMin, nums[i_ptr]); } // Now prefixMax/prefixMin are the min/max of nums[0...i_limit] long currentVal = nums[j]; long candidate1 = currentVal * prefixMax; long candidate2 = currentVal * prefixMin; maxProduct = Math.max(maxProduct, Math.max(candidate1, candidate2)); } return maxProduct; }}Complexity
Time
O(N) - Both pointers `j` and `i_ptr` traverse the array at most once from left to right. The total number of operations is proportional to N.
Space
O(1) - Only a few variables are used to store the running prefix min/max and pointers, independent of the input size.
Trade-offs
Pros
Optimal time complexity of O(N).
Optimal space complexity of O(1).
Efficiently solves the problem in a single pass.
Cons
The logic is slightly more complex to implement correctly compared to the O(N) space solution due to the coupled movement of the two pointers.
This approach involves a straightforward, nested-loop iteration to check all valid pairs of indices (i, j) that can form the start and end of a subsequence of size m. For each pair, we calculate the product of the corresponding elements and keep track of the maximum product seen so far. The condition to ensure a valid subsequence is that the distance between the indices j and i must be at least m - 1, allowing m - 2 other elements to be chosen from between them.
Algorithm
The brute-force approach systematically checks every possible pair of elements that could serve as the first and last elements of a valid subsequence.
- Initialize a variable
maxProductto a very small number (e.g.,Long.MIN_VALUE) to store the maximum product found. - Iterate through the array with an index
ifrom0tonums.length - 1. This indexirepresents the position of the first element of the subsequence. - For each
i, start a nested loop with an indexjfromi + m - 1tonums.length - 1. This indexjrepresents the position of the last element. - The condition
j >= i + m - 1ensures that there are at leastm - 2elements available between indicesiandjto form a subsequence of sizem. - Inside the inner loop, calculate the product of
nums[i]andnums[j]. Use alongto prevent overflow. - Compare this product with
maxProductand updatemaxProductif the current product is larger. - After the loops complete,
maxProductwill hold the maximum possible product.
Walkthrough
The core idea is to exhaustively search for the best pair of elements. We can select nums[i] as the first element and nums[j] as the last element of our subsequence. For this to be possible, we must be able to select m-2 additional elements from the original array that lie between nums[i] and nums[j]. The number of elements available between index i and j is j - i - 1. Thus, we must satisfy the condition j - i - 1 >= m - 2, which simplifies to j - i >= m - 1.
This leads to a simple algorithm where we iterate through all possible i and for each i, we iterate through all valid j's, compute the product, and update our answer.
class Solution { public long maximumProduct(int[] nums, int m) { int n = nums.length; if (m == 1) { long maxProd = Long.MIN_VALUE; for (int num : nums) { maxProd = Math.max(maxProd, (long) num * num); } return maxProd; } long maxProduct = Long.MIN_VALUE; for (int i = 0; i <= n - m; i++) { for (int j = i + m - 1; j < n; j++) { long product = (long) nums[i] * nums[j]; if (product > maxProduct) { maxProduct = product; } } } return maxProduct; }}Note: The special handling for m=1 can be merged into the main logic, as j >= i + 1 - 1 becomes j >= i. However, separating it can make the logic for the main case m > 1 clearer.
Complexity
Time
O(N^2) - where N is the number of elements in `nums`. The nested loops lead to a quadratic number of product calculations.
Space
O(1) - We only use a few variables to store the indices and the maximum product, regardless of the input size.
Trade-offs
Pros
Very simple to understand and implement.
Requires no extra space, making it memory efficient.
Cons
The time complexity of O(N^2) is too slow for the given constraints (N up to 10^5) and will likely result in a 'Time Limit Exceeded' error on most platforms.
We can significantly improve upon the brute-force approach by avoiding the nested loop. The key observation is that for a fixed first element nums[i], the best choice for the last element nums[j] (where j >= i + m - 1) is either the largest or smallest number in the valid range for j. Similarly, if we fix nums[j], the best nums[i] is the largest or smallest in its valid range. This suggests precomputing prefix (or suffix) minimums and maximums. By spending O(N) time and space on precomputation, we can reduce the main loop to a single pass, making the overall solution linear.
Algorithm
This approach optimizes the search for the second element. Instead of a nested loop, we precompute information about the array to find the best partner for each element in linear time.
- We can iterate through the array, fixing one element of the product pair, and then efficiently find the best possible second element.
- Let's fix the last element
nums[j]and iteratejfromm-1ton-1. - For a given
nums[j], the first elementnums[i]must be chosen from the subarraynums[0...k]wherek = j - m + 1. - To maximize
nums[i] * nums[j], we need either the maximum or minimum value fromnums[0...k], depending on the sign ofnums[j].- If
nums[j]is positive, we need the maximum value innums[0...k]. - If
nums[j]is negative, we need the minimum value innums[0...k].
- If
- We can precompute two arrays,
prefixMaxandprefixMin, in O(N) time.prefixMax[k]stores the maximum value innums[0...k], andprefixMin[k]stores the minimum. - With these precomputed arrays, we can iterate
jfromm-1ton-1, find the bestnums[i]in O(1) time, calculate the product, and update the overall maximum product.
Walkthrough
The problem is to maximize nums[i] * nums[j] subject to j - i >= m - 1. Let's iterate through all possible indices j for the last element. For each j, the index i for the first element must satisfy i <= j - m + 1. To maximize the product nums[i] * nums[j], we need to find the optimal nums[i] from the prefix nums[0...j-m+1].
If nums[j] is positive, we should pick the largest possible nums[i]. If nums[j] is negative, we should pick the smallest possible nums[i] (most negative) to make the product positive and large.
This can be solved efficiently by pre-calculating the minimum and maximum values for all prefixes of the array.
class Solution { public long maximumProduct(int[] nums, int m) { int n = nums.length; if (n < m) return 0; // Or handle as per problem spec for invalid input // The case m=1 is equivalent to finding max(num*num), which the general // logic below correctly handles as it becomes max(nums[i]*nums[j]) for i<=j. long[] prefixMax = new long[n]; long[] prefixMin = new long[n]; prefixMax[0] = nums[0]; prefixMin[0] = nums[0]; for (int i = 1; i < n; i++) { prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]); prefixMin[i] = Math.min(prefixMin[i - 1], nums[i]); } long maxProduct = Long.MIN_VALUE; // Iterate through all possible last elements `nums[j]` for (int j = m - 1; j < n; j++) { // The first element `nums[i]` must be in the range nums[0...j-m+1] int i_limit = j - m + 1; long pMax = prefixMax[i_limit]; long pMin = prefixMin[i_limit]; long currentVal = nums[j]; long candidate1 = currentVal * pMax; long candidate2 = currentVal * pMin; maxProduct = Math.max(maxProduct, Math.max(candidate1, candidate2)); } return maxProduct; }}An alternative symmetric solution involves precomputing suffix min/max arrays and iterating i from 0 to n-m.
Complexity
Time
O(N) - The solution involves two separate passes over the array: one for precomputation and one for finding the maximum product. Both are linear, so the total time is O(N).
Space
O(N) - We use two auxiliary arrays, `prefixMax` and `prefixMin`, each of size N, to store the precomputed values.
Trade-offs
Pros
Achieves a highly efficient linear time complexity O(N).
The logic is a standard dynamic programming/precomputation pattern.
Cons
Requires O(N) extra space, which might be a concern for very large inputs in memory-constrained environments.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.