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] from nums1 and [3, -6] from nums2, giving dot product 2*3 + 5*(-6) = 6 - 30 = -24
  • Choose subsequence [1] from nums1 and [0] from nums2, giving dot product 1*0 = 0
  • Choose subsequence [5] from nums1 and [3] from nums2, giving dot product 5*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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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] and nums2[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:

  1. Skip the current element from nums1: We use the best result from f[i-1][j], which means we've already considered all ways to pair the first i-1 elements of nums1 with the first j elements of nums2.

  2. Skip the current element from nums2: We use the best result from f[i][j-1], which means we've already considered all ways to pair the first i elements of nums1 with the first j-1 elements of nums2.

  3. 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 product f[i-1][j-1] is negative, we might want to start fresh with just the current pair. That's why we use max(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 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] 31
1class 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} 36
1class 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}; 33
1/** 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} 41

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 f with dimensions (m+1) × (n+1) where m = len(nums1) and n = 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 adding v to 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 Evaluator

Example 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 = 2
  • f[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) = -4
  • f[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) = -1
  • f[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 = 4
  • f[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 = 3
  • f[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) = -4
  • f[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.

Time 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] = 0 would allow the algorithm to return 0 even when all possible products are negative
  • For example, with nums1 = [-1] and nums2 = [-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:

  1. Empty subsequences are never considered valid (they have value -inf)
  2. The first product included will always be taken regardless of its sign
  3. 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.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More