3290. Maximum Multiplication Score
Problem Description
You have two integer arrays:
- Array
a
with exactly 4 elements - Array
b
with at least 4 elements
Your task is to select 4 indices from array b
: i₀
, i₁
, i₂
, and i₃
, where these indices must be in strictly increasing order (i₀ < i₁ < i₂ < i₃
).
Once you've selected these 4 indices, your score is calculated as:
score = a[0] * b[i₀] + a[1] * b[i₁] + a[2] * b[i₂] + a[3] * b[i₃]
The goal is to find the selection of indices that maximizes this score and return the maximum possible score.
For example, if a = [1, 2, 3, 4]
and b = [5, 6, 7, 8, 9]
, you could choose indices 0, 1, 2, 3 from array b
, giving you a score of 1*5 + 2*6 + 3*7 + 4*8 = 70
. Or you could choose different indices like 0, 1, 2, 4, giving you 1*5 + 2*6 + 3*7 + 4*9 = 74
, and so on. The problem asks you to find the maximum among all possible valid selections.
Intuition
The key insight is that we need to make a series of decisions: for each element in array a
, we need to pick a corresponding element from array b
, ensuring our picks are in increasing order of indices.
At any point in our decision-making process, we face a choice: should we use the current element from array b
or skip it and look for a better option later? This naturally leads to a recursive approach where we explore both possibilities.
Think of it as having two pointers: one for array a
(tracking which element we're trying to match) and one for array b
(tracking which element we're considering). At each step:
- We can skip the current element in
b
and move to the next one (keeping the same position ina
) - We can pair the current elements from both arrays and move both pointers forward
Since we need exactly 4 elements from b
in increasing order to match with the 4 elements of a
, we're essentially finding the optimal subsequence of length 4 from array b
that maximizes the dot product with array a
.
The recursive nature of this problem becomes clear when we realize that after making a choice at position (i, j)
, we face a similar but smaller problem at the next positions. This overlapping subproblem structure makes it perfect for dynamic programming with memoization.
The function dfs(i, j)
represents: "What's the maximum score I can get if I still need to match elements from position i
onwards in array a
with elements from position j
onwards in array b
?" By caching these results, we avoid recalculating the same subproblems multiple times.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses a recursive approach with memoization to find the maximum score. Here's how the implementation works:
We define a recursive function dfs(i, j)
where:
i
represents the current index in arraya
(which element we're trying to match)j
represents the current index in arrayb
(which element we're considering)
The function returns the maximum score achievable starting from these positions.
Base Cases:
-
If
j >= len(b)
: We've exhausted arrayb
. If we've also matched all elements ofa
(i.e.,i >= len(a)
), we return0
as we've successfully formed a valid selection. Otherwise, we return-inf
because we can't complete the matching. -
If
i >= len(a)
: We've successfully matched all elements of arraya
, so we return0
.
Recursive Cases:
For each position (i, j)
, we have two choices:
- Skip the current element in
b
: Calldfs(i, j + 1)
to consider the next element inb
while staying at the same position ina
- Use the current element in
b
: Calculatea[i] * b[j]
and add it todfs(i + 1, j + 1)
, which represents matching the current elements and moving forward in both arrays
We take the maximum of these two options: max(dfs(i, j + 1), a[i] * b[j] + dfs(i + 1, j + 1))
Memoization:
The @cache
decorator automatically memoizes the function results. This prevents redundant calculations when the same (i, j)
pair is encountered multiple times through different recursive paths, reducing the time complexity from exponential to polynomial.
Starting Point:
The solution begins with dfs(0, 0)
, which finds the maximum score starting from the beginning of both arrays.
The algorithm essentially explores all valid ways to select 4 indices from array b
in increasing order, calculating the score for each valid selection, and returns the maximum among all possibilities.
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 to illustrate the solution approach.
Given:
a = [2, 3, 1, 4]
b = [1, 5, 2, 3, 6]
We need to select 4 indices from b
in strictly increasing order to maximize the score.
Starting with dfs(0, 0)
:
- We're at position 0 in array
a
(value 2) and position 0 in arrayb
(value 1) - We have two choices:
- Skip b[0]: Call
dfs(0, 1)
- look for a better match for a[0] starting from b[1] - Use b[0]: Score = 2 × 1 = 2, then call
dfs(1, 1)
to match remaining elements
- Skip b[0]: Call
Let's trace the path that leads to the optimal solution:
Path to optimal: indices 0, 1, 3, 4 from b
dfs(0, 0)
: Use b[0] → score = 2 × 1 = 2, continue withdfs(1, 1)
dfs(1, 1)
: Use b[1] → score = 3 × 5 = 15, continue withdfs(2, 2)
dfs(2, 2)
: Skip b[2], trydfs(2, 3)
dfs(2, 3)
: Use b[3] → score = 1 × 3 = 3, continue withdfs(3, 4)
dfs(3, 4)
: Use b[4] → score = 4 × 6 = 24, continue withdfs(4, 5)
dfs(4, 5)
: Base case (i ≥ len(a)) → return 0
Total score = 2 + 15 + 3 + 24 = 44
Alternative path explored: indices 1, 2, 3, 4
dfs(0, 0)
: Skip b[0], trydfs(0, 1)
dfs(0, 1)
: Use b[1] → score = 2 × 5 = 10, continue withdfs(1, 2)
dfs(1, 2)
: Use b[2] → score = 3 × 2 = 6, continue withdfs(2, 3)
dfs(2, 3)
: Use b[3] → score = 1 × 3 = 3, continue withdfs(3, 4)
dfs(3, 4)
: Use b[4] → score = 4 × 6 = 24, continue withdfs(4, 5)
dfs(4, 5)
: Base case → return 0
Total score = 10 + 6 + 3 + 24 = 43
The algorithm explores all valid combinations through the recursive calls, using memoization to avoid recalculating identical subproblems. When dfs(2, 3)
is called from different paths, for instance, it's only computed once and the cached result is reused.
The maximum score of 44 is returned, corresponding to selecting indices [0, 1, 3, 4] from array b
.
Solution Implementation
1from typing import List
2from functools import cache
3from math import inf
4
5class Solution:
6 def maxScore(self, a: List[int], b: List[int]) -> int:
7 """
8 Finds the maximum score by selecting elements from array b to multiply with array a.
9
10 Args:
11 a: List of multipliers
12 b: List of values to select from
13
14 Returns:
15 Maximum possible score
16 """
17
18 @cache
19 def dfs(i: int, j: int) -> int:
20 """
21 Dynamic programming helper function to find maximum score.
22
23 Args:
24 i: Current index in array a (multipliers)
25 j: Current index in array b (values to select)
26
27 Returns:
28 Maximum score achievable from current state
29 """
30 # Base case: if we've exhausted array b
31 if j >= len(b):
32 # If we still have multipliers left, it's invalid
33 return 0 if i >= len(a) else -inf
34
35 # Base case: if we've used all multipliers, score is 0
36 if i >= len(a):
37 return 0
38
39 # Two choices at each step:
40 # 1. Skip current element in b
41 skip_current = dfs(i, j + 1)
42
43 # 2. Use current element in b with current multiplier in a
44 use_current = a[i] * b[j] + dfs(i + 1, j + 1)
45
46 return max(skip_current, use_current)
47
48 return dfs(0, 0)
49
1class Solution {
2 // Memoization table to store computed results
3 // dp[i][j] represents the maximum score when considering a[i..] and b[j..]
4 private Long[][] dp;
5
6 // Input arrays
7 private int[] arrayA;
8 private int[] arrayB;
9
10 /**
11 * Calculates the maximum score by selecting elements from array b
12 * and multiplying them with corresponding elements from array a
13 * @param a - array of multipliers
14 * @param b - array of values to select from
15 * @return maximum possible score
16 */
17 public long maxScore(int[] a, int[] b) {
18 // Initialize memoization table
19 dp = new Long[a.length][b.length];
20
21 // Store input arrays as instance variables
22 this.arrayA = a;
23 this.arrayB = b;
24
25 // Start recursive calculation from index 0 for both arrays
26 return dfs(0, 0);
27 }
28
29 /**
30 * Recursive function with memoization to find maximum score
31 * @param indexA - current index in array a
32 * @param indexB - current index in array b
33 * @return maximum score achievable from current position
34 */
35 private long dfs(int indexA, int indexB) {
36 // Base case: if we've exhausted array b
37 if (indexB >= arrayB.length) {
38 // If we haven't used all elements of array a, return invalid score
39 // Otherwise, return 0 (successful completion)
40 return indexA >= arrayA.length ? 0 : Long.MIN_VALUE / 2;
41 }
42
43 // Base case: if we've used all elements of array a
44 if (indexA >= arrayA.length) {
45 // Successfully matched all elements from array a
46 return 0;
47 }
48
49 // Check if result is already computed (memoization)
50 if (dp[indexA][indexB] != null) {
51 return dp[indexA][indexB];
52 }
53
54 // Calculate and store the maximum of two choices:
55 // 1. Skip current element in array b (move to next b element)
56 long skipCurrentB = dfs(indexA, indexB + 1);
57
58 // 2. Use current element from both arrays (multiply and move both indices)
59 long useCurrentPair = 1L * arrayA[indexA] * arrayB[indexB] + dfs(indexA + 1, indexB + 1);
60
61 // Store and return the maximum score
62 return dp[indexA][indexB] = Math.max(skipCurrentB, useCurrentPair);
63 }
64}
65
1class Solution {
2public:
3 long long maxScore(vector<int>& a, vector<int>& b) {
4 int aSize = a.size();
5 int bSize = b.size();
6
7 // Create memoization table to store intermediate results
8 // memo[i][j] represents the maximum score starting from index i in array a and index j in array b
9 vector<vector<long long>> memo(aSize, vector<long long>(bSize, -1));
10
11 // Recursive function with memoization to find maximum score
12 auto calculateMaxScore = [&](this auto&& calculateMaxScore, int aIndex, int bIndex) -> long long {
13 // Base case: if we've exhausted array b
14 if (bIndex >= bSize) {
15 // If we haven't used all elements from array a, return a very small value (invalid state)
16 // Otherwise, return 0 (successfully matched all elements from a)
17 return aIndex >= aSize ? 0 : LLONG_MIN / 2;
18 }
19
20 // Base case: if we've used all elements from array a, return 0
21 if (aIndex >= aSize) {
22 return 0;
23 }
24
25 // Check if this state has already been computed
26 if (memo[aIndex][bIndex] != -1) {
27 return memo[aIndex][bIndex];
28 }
29
30 // Two choices at each step:
31 // 1. Skip current element in array b and move to next element in b
32 long long skipCurrent = calculateMaxScore(aIndex, bIndex + 1);
33
34 // 2. Use current element in b with current element in a, then move to next elements in both arrays
35 long long useCurrent = 1LL * a[aIndex] * b[bIndex] + calculateMaxScore(aIndex + 1, bIndex + 1);
36
37 // Store and return the maximum of both choices
38 return memo[aIndex][bIndex] = max(skipCurrent, useCurrent);
39 };
40
41 // Start the recursive calculation from the beginning of both arrays
42 return calculateMaxScore(0, 0);
43 }
44};
45
1/**
2 * Calculates the maximum score by selecting elements from array b and multiplying
3 * them with corresponding elements from array a in order.
4 * @param a - Array of multipliers that must be used in order
5 * @param b - Array of values to select from
6 * @returns Maximum possible score
7 */
8function maxScore(a: number[], b: number[]): number {
9 const multiplierCount: number = a.length;
10 const valueCount: number = b.length;
11
12 // Memoization table: memo[i][j] stores the maximum score when considering
13 // multipliers from index i onwards and values from index j onwards
14 const memo: number[][] = Array.from(
15 { length: multiplierCount },
16 () => Array(valueCount).fill(-1)
17 );
18
19 /**
20 * Recursive function with memoization to find maximum score
21 * @param multiplierIndex - Current index in the multiplier array (a)
22 * @param valueIndex - Current index in the value array (b)
23 * @returns Maximum score achievable from current state
24 */
25 const findMaxScore = (multiplierIndex: number, valueIndex: number): number => {
26 // Base case: No more values available but multipliers remain
27 if (valueIndex >= valueCount) {
28 return multiplierIndex >= multiplierCount ? 0 : -Infinity;
29 }
30
31 // Base case: All multipliers have been used
32 if (multiplierIndex >= multiplierCount) {
33 return 0;
34 }
35
36 // Return memoized result if already computed
37 if (memo[multiplierIndex][valueIndex] !== -1) {
38 return memo[multiplierIndex][valueIndex];
39 }
40
41 // Choice 1: Skip current value in b array
42 const skipCurrentValue: number = findMaxScore(multiplierIndex, valueIndex + 1);
43
44 // Choice 2: Use current value with current multiplier
45 const useCurrentValue: number = a[multiplierIndex] * b[valueIndex] +
46 findMaxScore(multiplierIndex + 1, valueIndex + 1);
47
48 // Store and return the maximum of both choices
49 memo[multiplierIndex][valueIndex] = Math.max(skipCurrentValue, useCurrentValue);
50 return memo[multiplierIndex][valueIndex];
51 };
52
53 return findMaxScore(0, 0);
54}
55
Time and Space Complexity
The time complexity is O(m × n)
, where m
is the length of array a
and n
is the length of array b
. This is because the recursive function dfs(i, j)
explores all possible combinations of indices i
(ranging from 0 to m
) and j
(ranging from 0 to n
). Due to the @cache
decorator, each unique state (i, j)
is computed only once, resulting in at most (m + 1) × (n + 1)
different states to compute.
The space complexity is O(m × n)
. This comes from two sources:
- The memoization cache stores up to
(m + 1) × (n + 1)
entries, which isO(m × n)
space. - The recursion depth in the worst case is
O(min(m, n))
for the call stack, but this is dominated by the cache storage.
Therefore, the overall space complexity is O(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow with Large Numbers
When dealing with multiplication of potentially large integers, the product a[i] * b[j]
could exceed typical integer bounds in some programming languages. While Python handles arbitrary precision integers automatically, this could be problematic in languages like Java or C++.
Solution: In languages with fixed integer sizes, use appropriate data types (e.g., long long
in C++ or Long
in Java) to handle large products.
2. Incorrect Base Case Handling
A common mistake is not properly handling the base cases, particularly when j >= len(b)
. The condition return 0 if i >= len(a) else -inf
is crucial. Returning 0 when we haven't matched all elements of a
would incorrectly suggest a valid solution exists.
Solution: Always return negative infinity (-inf
) when we've exhausted array b
but still have unmatched elements in array a
. This ensures invalid paths are properly rejected when taking the maximum.
3. Off-by-One Errors in Index Boundaries
It's easy to confuse whether to use >
or >=
when checking array boundaries. Using j > len(b)
instead of j >= len(b)
would cause an index out of bounds error.
Solution: Remember that array indices are 0-based, so j >= len(b)
correctly identifies when we've moved past the last valid index.
4. Forgetting to Clear Cache Between Test Cases
In competitive programming or when running multiple test cases, the @cache
decorator persists memoized values between function calls. This can lead to incorrect results if the arrays change between test cases.
Solution: Either:
- Move the cached function definition inside the main function so it's recreated for each test case
- Manually clear the cache using
dfs.cache_clear()
before each new test case - Use a dictionary-based memoization that's local to each function call
5. Not Considering Negative Numbers
The algorithm works correctly with negative numbers, but developers might incorrectly assume all values are positive and add unnecessary constraints or optimizations that break with negative values.
Solution: Test with arrays containing negative numbers to ensure the solution handles all cases correctly. The algorithm naturally handles negatives since it explores all possibilities and selects the maximum.
Which two pointer techniques do you use to check if a string is a palindrome?
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!