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]
fromnums1
and[3, -6]
fromnums2
, giving dot product2*3 + 5*(-6) = 6 - 30 = -24
- Choose subsequence
[1]
fromnums1
and[0]
fromnums2
, giving dot product1*0 = 0
- Choose subsequence
[5]
fromnums1
and[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-1
elements ofnums1
with the firstj
elements 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 firsti
elements ofnums1
with the firstj-1
elements 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
f
with 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 addingv
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 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 = 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
.
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
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]
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 is a good use case for backtracking?
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 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
Want a Structured Path to Master System Design Too? Don’t Miss This!