1458. Max Dot Product of Two Subsequences
Problem Description
You are given two arrays nums1 and nums2.
Your task is to find the maximum dot product between non-empty subsequences of nums1 and nums2 that have the same length.
The dot product of two sequences [a1, a2, ..., ak] and [b1, b2, ..., bk] is calculated as a1*b1 + a2*b2 + ... + ak*bk.
A subsequence is formed by deleting some (possibly zero) elements from the original array without changing the relative order of the remaining elements. For example, [2,3,5] is a subsequence of [1,2,3,4,5], but [1,5,3] is not because it doesn't maintain the relative order.
The key requirements are:
- Both subsequences must be non-empty
- Both subsequences must have the same length
- You want to maximize the dot product
For example, if nums1 = [2, 1, -2, 5] and nums2 = [3, 0, -6], you could:
- Choose subsequence
[2, 5]fromnums1and[3, -6]fromnums2, giving dot product2*3 + 5*(-6) = 6 - 30 = -24 - Choose subsequence
[1]fromnums1and[0]fromnums2, giving dot product1*0 = 0 - Choose subsequence
[5]fromnums1and[3]fromnums2, giving dot product5*3 = 15
The maximum dot product would be 18 by choosing [2, 1] from nums1 and [3, -6] from nums2, giving 2*3 + 1*(-6) = 6 + 12 = 18.
Intuition
When we need to find optimal subsequences from two arrays, dynamic programming is often a natural choice because we can build up the solution by considering smaller subproblems.
Think about what choices we have at each step. For any position i in nums1 and position j in nums2, we need to decide:
- Should we include both
nums1[i]andnums2[j]in our subsequences? - Or should we skip one of them?
This leads us to define our state as f[i][j] - the maximum dot product we can achieve using elements from the first i elements of nums1 and the first j elements of nums2.
For each pair of positions (i, j), we have three main choices:
-
Skip the current element from
nums1: We use the best result fromf[i-1][j], which means we've already considered all ways to pair the firsti-1elements ofnums1with the firstjelements ofnums2. -
Skip the current element from
nums2: We use the best result fromf[i][j-1], which means we've already considered all ways to pair the firstielements ofnums1with the firstj-1elements ofnums2. -
Pair the current elements together: We multiply
nums1[i-1] * nums2[j-1]and add it to the best dot product we could achieve with the previous elements. Here's a crucial insight: if the previous best dot productf[i-1][j-1]is negative, we might want to start fresh with just the current pair. That's why we usemax(0, f[i-1][j-1]) + nums1[i-1] * nums2[j-1].
The reason we take max(0, f[i-1][j-1]) is because if all previous combinations gave us negative results, we're better off starting a new subsequence with just the current pair rather than adding to a negative sum.
By considering all these options at each step and taking the maximum, we ensure we find the optimal solution. The final answer is stored in f[m][n], representing the maximum dot product using all available elements from both arrays.
Learn more about Dynamic Programming patterns.
Solution Approach
We implement the solution using a 2D dynamic programming table f where f[i][j] represents the maximum dot product between subsequences formed from the first i elements of nums1 and the first j elements of nums2.
Initialization:
- Create a 2D table
fwith dimensions(m+1) × (n+1)wherem = len(nums1)andn = len(nums2) - Initialize all values to negative infinity (
-inf) since we need to find the maximum and haven't computed any valid dot products yet - The extra row and column (index 0) serve as base cases representing empty subsequences
State Transition:
For each position (i, j) where 1 ≤ i ≤ m and 1 ≤ j ≤ n, we compute:
v = nums1[i-1] * nums2[j-1] # Product of current elements
f[i][j] = max(
f[i-1][j], # Skip nums1[i-1]
f[i][j-1], # Skip nums2[j-1]
max(0, f[i-1][j-1]) + v # Include both current elements
)
The key insight in the third option is using max(0, f[i-1][j-1]) + v:
- If
f[i-1][j-1]is positive, we extend the previous subsequence by adding the current pair - If
f[i-1][j-1]is negative or-inf, we start a new subsequence with just the current pair (equivalent to addingvto 0)
Iteration Order:
We iterate through each element of nums1 (outer loop) and for each, iterate through all elements of nums2 (inner loop). This ensures that when we compute f[i][j], the values f[i-1][j], f[i][j-1], and f[i-1][j-1] have already been computed.
Final Result:
The answer is f[m][n], which represents the maximum dot product considering all elements from both arrays.
Time Complexity: O(m × n) where m and n are the lengths of the two arrays
Space Complexity: O(m × n) for the DP table
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 a small example with nums1 = [2, -1, 3] and nums2 = [1, -2].
We'll build a DP table f of size 4×3 (including base cases). Initially, all values are set to -inf.
Step-by-step computation:
For i=1, j=1 (considering nums1[0]=2 and nums2[0]=1):
- Product
v = 2 * 1 = 2 f[0][1]= -inf (skip nums1[0])f[1][0]= -inf (skip nums2[0])max(0, f[0][0]) + v = max(0, -inf) + 2 = 0 + 2 = 2f[1][1] = max(-inf, -inf, 2) = 2
For i=1, j=2 (considering nums1[0]=2 and nums2[1]=-2):
- Product
v = 2 * (-2) = -4 f[0][2]= -inf (skip nums1[0])f[1][1]= 2 (skip nums2[1])max(0, f[0][1]) + v = max(0, -inf) + (-4) = 0 + (-4) = -4f[1][2] = max(-inf, 2, -4) = 2
For i=2, j=1 (considering nums1[1]=-1 and nums2[0]=1):
- Product
v = -1 * 1 = -1 f[1][1]= 2 (skip nums1[1])f[2][0]= -inf (skip nums2[0])max(0, f[1][0]) + v = max(0, -inf) + (-1) = 0 + (-1) = -1f[2][1] = max(2, -inf, -1) = 2
For i=2, j=2 (considering nums1[1]=-1 and nums2[1]=-2):
- Product
v = -1 * (-2) = 2 f[1][2]= 2 (skip nums1[1])f[2][1]= 2 (skip nums2[1])max(0, f[1][1]) + v = max(0, 2) + 2 = 2 + 2 = 4f[2][2] = max(2, 2, 4) = 4
For i=3, j=1 (considering nums1[2]=3 and nums2[0]=1):
- Product
v = 3 * 1 = 3 f[2][1]= 2 (skip nums1[2])f[3][0]= -inf (skip nums2[0])max(0, f[2][0]) + v = max(0, -inf) + 3 = 0 + 3 = 3f[3][1] = max(2, -inf, 3) = 3
For i=3, j=2 (considering nums1[2]=3 and nums2[1]=-2):
- Product
v = 3 * (-2) = -6 f[2][2]= 4 (skip nums1[2])f[3][1]= 3 (skip nums2[1])max(0, f[2][1]) + v = max(0, 2) + (-6) = 2 + (-6) = -4f[3][2] = max(4, 3, -4) = 4
Final DP table:
j=0 j=1 j=2 i=0 -inf -inf -inf i=1 -inf 2 2 i=2 -inf 2 4 i=3 -inf 3 4
The maximum dot product is f[3][2] = 4, which corresponds to choosing subsequence [2, -1] from nums1 and subsequence [1, -2] from nums2, giving us 2*1 + (-1)*(-2) = 2 + 2 = 4.
Solution Implementation
1class Solution:
2 def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
3 # Get the lengths of both arrays
4 m, n = len(nums1), len(nums2)
5
6 # Initialize DP table with negative infinity
7 # dp[i][j] represents the maximum dot product using first i elements from nums1
8 # and first j elements from nums2
9 dp = [[float('-inf')] * (n + 1) for _ in range(m + 1)]
10
11 # Iterate through each element in nums1
12 for i in range(1, m + 1):
13 # Iterate through each element in nums2
14 for j in range(1, n + 1):
15 # Calculate the product of current elements
16 current_product = nums1[i - 1] * nums2[j - 1]
17
18 # Take maximum of three cases:
19 # 1. Skip current element from nums1: dp[i-1][j]
20 # 2. Skip current element from nums2: dp[i][j-1]
21 # 3. Include current pair: max(0, dp[i-1][j-1]) + current_product
22 # (we take max with 0 to optionally start a new subsequence)
23 dp[i][j] = max(
24 dp[i - 1][j], # Skip nums1[i-1]
25 dp[i][j - 1], # Skip nums2[j-1]
26 max(0, dp[i - 1][j - 1]) + current_product # Include both elements
27 )
28
29 # Return the maximum dot product using all available elements
30 return dp[m][n]
311class Solution {
2 public int maxDotProduct(int[] nums1, int[] nums2) {
3 int m = nums1.length;
4 int n = nums2.length;
5
6 // dp[i][j] represents the maximum dot product of subsequences
7 // from nums1[0...i-1] and nums2[0...j-1]
8 int[][] dp = new int[m + 1][n + 1];
9
10 // Initialize with minimum value as we need at least one pair
11 for (int[] row : dp) {
12 Arrays.fill(row, Integer.MIN_VALUE);
13 }
14
15 // Fill the DP table
16 for (int i = 1; i <= m; i++) {
17 for (int j = 1; j <= n; j++) {
18 // Current product of nums1[i-1] and nums2[j-1]
19 int currentProduct = nums1[i - 1] * nums2[j - 1];
20
21 // Option 1: Skip current element from nums1
22 dp[i][j] = Math.max(dp[i][j], dp[i - 1][j]);
23
24 // Option 2: Skip current element from nums2
25 dp[i][j] = Math.max(dp[i][j], dp[i][j - 1]);
26
27 // Option 3: Include current pair
28 // Either start new subsequence with current pair or extend previous subsequence
29 dp[i][j] = Math.max(dp[i][j], Math.max(dp[i - 1][j - 1], 0) + currentProduct);
30 }
31 }
32
33 return dp[m][n];
34 }
35}
361class Solution {
2public:
3 int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
4 int m = nums1.size();
5 int n = nums2.size();
6
7 // dp[i][j] represents the maximum dot product of subsequences
8 // from nums1[0...i-1] and nums2[0...j-1]
9 vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MIN));
10
11 // Iterate through all pairs of elements
12 for (int i = 1; i <= m; ++i) {
13 for (int j = 1; j <= n; ++j) {
14 // Calculate the product of current pair
15 int currentProduct = nums1[i - 1] * nums2[j - 1];
16
17 // Option 1: Skip element from nums1 (take dp[i-1][j])
18 dp[i][j] = dp[i - 1][j];
19
20 // Option 2: Skip element from nums2 (take dp[i][j-1])
21 dp[i][j] = max(dp[i][j], dp[i][j - 1]);
22
23 // Option 3: Include current pair
24 // Either start new subsequence with current pair only,
25 // or extend previous subsequence dp[i-1][j-1] with current pair
26 dp[i][j] = max(dp[i][j], max(0, dp[i - 1][j - 1]) + currentProduct);
27 }
28 }
29
30 return dp[m][n];
31 }
32};
331/**
2 * Finds the maximum dot product between two non-empty subsequences of nums1 and nums2
3 * with the same non-zero length.
4 *
5 * @param nums1 - First array of integers
6 * @param nums2 - Second array of integers
7 * @returns The maximum dot product between subsequences
8 */
9function maxDotProduct(nums1: number[], nums2: number[]): number {
10 const firstArrayLength: number = nums1.length;
11 const secondArrayLength: number = nums2.length;
12
13 // Initialize DP table with -Infinity
14 // dp[i][j] represents the maximum dot product using first i elements of nums1
15 // and first j elements of nums2
16 const dp: number[][] = Array.from(
17 { length: firstArrayLength + 1 },
18 () => Array.from({ length: secondArrayLength + 1 }, () => -Infinity)
19 );
20
21 // Fill the DP table
22 for (let i = 1; i <= firstArrayLength; i++) {
23 for (let j = 1; j <= secondArrayLength; j++) {
24 // Calculate the product of current elements
25 const currentProduct: number = nums1[i - 1] * nums2[j - 1];
26
27 // Option 1: Skip current element from nums1 or nums2
28 dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
29
30 // Option 2: Include current elements in the subsequence
31 // Either start a new subsequence with current elements or extend previous subsequence
32 dp[i][j] = Math.max(
33 dp[i][j],
34 Math.max(0, dp[i - 1][j - 1]) + currentProduct
35 );
36 }
37 }
38
39 return dp[firstArrayLength][secondArrayLength];
40}
41Time and Space Complexity
The time complexity is O(m × n), where m is the length of nums1 and n is the length of nums2. This is because the code uses two nested loops: the outer loop iterates through all elements of nums1 (from index 1 to m), and the inner loop iterates through all elements of nums2 (from index 1 to n). Each cell f[i][j] is computed exactly once with constant time operations (multiplication and max comparisons), resulting in m × n total operations.
The space complexity is O(m × n). The code creates a 2D dynamic programming table f with dimensions (m + 1) × (n + 1) to store the intermediate results. Each cell stores a single value representing the maximum dot product for the corresponding subproblems. The additional +1 in each dimension accounts for the base cases (empty subsequences), but this doesn't change the asymptotic complexity, which remains O(m × n).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Incorrect Initialization Leading to Wrong Results for All-Negative Arrays
The Problem:
The current solution initializes the DP table with float('-inf') and uses the recurrence relation:
dp[i][j] = max(
dp[i - 1][j],
dp[i][j - 1],
max(0, dp[i - 1][j - 1]) + current_product
)
This works correctly in most cases, but there's a subtle issue. When dp[i-1][j-1] is -inf (which happens when we haven't selected any pairs yet), the expression max(0, dp[i-1][j-1]) + current_product evaluates to 0 + current_product = current_product, which correctly starts a new subsequence.
However, the pitfall occurs when someone tries to "optimize" or modify this initialization. A common mistake is to initialize dp[0][0] = 0 thinking it represents "no elements selected, so dot product is 0". This breaks the logic because:
- The problem requires non-empty subsequences
- Setting
dp[0][0] = 0would allow the algorithm to return 0 even when all possible products are negative - For example, with
nums1 = [-1]andnums2 = [-1], the only valid subsequence pair gives dot product = 1, but incorrect initialization might return 0
Example of Incorrect Code:
# WRONG: This initialization can lead to incorrect results
dp = [[0] * (n + 1) for _ in range(m + 1)] # Initializing with 0 instead of -inf
The Solution: Maintain the initialization with negative infinity for all positions except when computing actual products. This ensures:
- Empty subsequences are never considered valid (they have value
-inf) - The first product included will always be taken regardless of its sign
- The algorithm correctly handles all-negative or all-positive cases
Correct Approach:
# Initialize everything with -inf
dp = [[float('-inf')] * (n + 1) for _ in range(m + 1)]
# In the recurrence, max(0, dp[i-1][j-1]) handles both:
# - Starting a new subsequence (when dp[i-1][j-1] is -inf)
# - Extending an existing subsequence (when dp[i-1][j-1] is a valid value)
Alternative Clear Solution: If the logic seems confusing, you can make it more explicit:
for i in range(1, m + 1):
for j in range(1, n + 1):
current_product = nums1[i - 1] * nums2[j - 1]
# Start with skipping options
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# Include current pair
if dp[i - 1][j - 1] == float('-inf'):
# First pair in subsequence
dp[i][j] = max(dp[i][j], current_product)
else:
# Extend existing subsequence
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + current_product)
This makes it clear that we're handling the "first pair" case separately from the "extending subsequence" case.
Which of the following array represent a max heap?
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
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
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way 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 assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!