Maximum Score from Performing Multiplication
Problem
You are given two integer arrays nums and multipliers of size n and m respectively, where n >= m. The arrays are 1-indexed.
You begin with a score of 0. You want to perform exactly m operations. On the ith operation (1-indexed), you will:
- Choose one integer
xfrom either the start or the end of the arraynums. - Add
multipliers[i] * xto your score. - Remove
xfrom the arraynums.
Return the maximum score after performing m operations.
Examples
Example 1:
Input:
nums = [1,2,3], multipliers = [3,2,1]
Output:
14
Explanation: An optimal solution is as follows:
- Choose from the end, [1,2,**3**], adding 3 * 3 = 9 to the score.
- Choose from the end, [1,**2**], adding 2 * 2 = 4 to the score.
- Choose from the end, [**1**], adding 1 * 1 = 1 to the score.
The total score is 9 + 4 + 1 = 14.
Example 2:
Input:
nums = [-5,-3,-3,-2,7,1], multipliers = [-10,-5,3,4,6]
Output:
102
Explanation: An optimal solution is as follows:
- Choose from the start, [**-5**,-3,-3,-2,7,1], adding -5 * -10 = 50 to the score.
- Choose from the start, [**-3**,-3,-2,7,1], adding -3 * -5 = 15 to the score.
- Choose from the start, [**-3**,-2,7,1], adding -3 * 3 = -9 to the score.
- Choose from the end, [-2,7,**1**], adding 1 * 4 = 4 to the score.
- Choose from the end, [-2,**7**], adding 7 * 6 = 42 to the score.
The total score is 50 + 15 - 9 + 4 + 42 = 102.
Solution
Method 1 - Try Greedy (which Fails ❌)
We can use two pointers, left and right, for the two ends of the array. At each step, we pick an integer x from either end of nums and multiply it by multipliers[i]. The idea is to always pick the larger x from either end.
Is this correct? Not always! Since both nums[i] and multipliers[i] can be negative (as per the constraints -1000 ≤ nums[i], multipliers[i] ≤ 1000), sometimes picking a smaller absolute value or a negative number is better, depending on the sign of multipliers[i]. For example, if multipliers[i] is -10, choosing -3 gives 30, which is better than choosing -5 and getting 50 (since we want the maximum score).
Dry Run 1
Input: nums = [1, 2, 3], multipliers = [3, 2, 1]
Step-by-step:
- For
multipliers[0] = 3, choosenums[2] = 3(end), add3 * 3 = 9. - For
multipliers[1] = 2, choosenums[1] = 2(end), add2 * 2 = 4. - For
multipliers[2] = 1, choosenums[0] = 1(end), add1 * 1 = 1.
Total score: 9 + 4 + 1 = 14 (Correct ✅)
Dry Run 2
Input: nums = [-5, -3, -3, -2, 7, 1], multipliers = [-10, -5, 3, 4, 6]
Step-by-step:
- For
multipliers[0] = -10, choosenums[0] = -5(start), add-5 * -10 = 50. - For
multipliers[1] = -5, choosenums[0] = -3(start), add-3 * -5 = 15. - For
multipliers[2] = 3, choosenums[3] = 1(end), add1 * 3 = 3. - For
multipliers[3] = 4, choosenums[2] = 7(end), add7 * 4 = 28. - For
multipliers[4] = 6, choosenums[1] = -2(end), add-2 * 6 = -12.
Total score: 50 + 15 + 3 + 28 + (-12) = 84, which is not optimal (the correct answer is 102). ❌
Greedy Failed
This was discussed on Computer Science Stack Exchange, and the answer and comments note:
- "To show that the greedy approach fails, all you have to do is come up with a counterexample."
- "To prove that the greedy algorithm is not optimal, it suffices to give one counterexample."
- "To prove that the greedy algorithm is not optimal, it suffices to give one counterexample"
Method 2 - Top Down DP
Intuition
The greedy approach fails because the optimal choice at each step depends on future decisions. Dynamic programming works here because the problem has overlapping subproblems and optimal substructure: the best score for a given state depends only on the choices made so far, not on the order. By storing results for each state, we avoid redundant calculations.
Approach
- Use recursion with memoization to explore all possible ways to pick elements from either end of
numsfor each operation. - Define the state by two variables:
i: number of operations performed so far (index inmultipliers)l: number of elements taken from the left ofnums- The right index is calculated as
r = n - 1 - (i - l)
- At each step, choose either:
- Take from the left: add
nums[l] * multipliers[i]and recurse withi+1, l+1 - Take from the right: add
nums[r] * multipliers[i]and recurse withi+1, l
- Take from the left: add
- Memoize results for each
(i, l)to avoid recomputation. - Base case: if all operations are done (
i == m), return 0.
The recurrence relation for this DP can be visualized as:
Code
C++
class Solution {
public:
int maximumScore(vector<int>& nums, vector<int>& multipliers) {
int n = nums.size(), m = multipliers.size();
vector<vector<int>> memo(m+1, vector<int>(m+1, INT_MIN));
function<int(int,int)> dfs = [&](int i, int l) {
if (i == m) return 0;
if (memo[i][l] != INT_MIN) return memo[i][l];
int r = n - 1 - (i - l);
int left = nums[l] * multipliers[i] + dfs(i+1, l+1);
int right = nums[r] * multipliers[i] + dfs(i+1, l);
return memo[i][l] = max(left, right);
};
return dfs(0, 0);
}
};
Go
func maximumScore(nums []int, multipliers []int) int {
n, m := len(nums), len(multipliers)
memo := make([][]int, m+1)
for i := range memo {
memo[i] = make([]int, m+1)
for j := range memo[i] {
memo[i][j] = -1 << 31
}
}
var dfs func(i, l int) int
dfs = func(i, l int) int {
if i == m {
return 0
}
if memo[i][l] != -1<<31 {
return memo[i][l]
}
r := n - 1 - (i - l)
left := nums[l]*multipliers[i] + dfs(i+1, l+1)
right := nums[r]*multipliers[i] + dfs(i+1, l)
if left > right {
memo[i][l] = left
} else {
memo[i][l] = right
}
return memo[i][l]
}
return dfs(0, 0)
}
Java
class Solution {
public int maximumScore(int[] nums, int[] multipliers) {
int n = nums.length, m = multipliers.length;
Integer[][] memo = new Integer[m+1][m+1];
return dfs(nums, multipliers, 0, 0, memo);
}
private int dfs(int[] nums, int[] multipliers, int i, int l, Integer[][] memo) {
if (i == multipliers.length) return 0;
if (memo[i][l] != null) return memo[i][l];
int r = nums.length - 1 - (i - l);
int left = nums[l] * multipliers[i] + dfs(nums, multipliers, i+1, l+1, memo);
int right = nums[r] * multipliers[i] + dfs(nums, multipliers, i+1, l, memo);
return memo[i][l] = Math.max(left, right);
}
}
Kotlin
class Solution {
fun maximumScore(nums: IntArray, multipliers: IntArray): Int {
val n = nums.size
val m = multipliers.size
val memo = Array(m+1) { Array(m+1) { null as Int? } }
fun dfs(i: Int, l: Int): Int {
if (i == m) return 0
memo[i][l]?.let { return it }
val r = n - 1 - (i - l)
val left = nums[l] * multipliers[i] + dfs(i+1, l+1)
val right = nums[r] * multipliers[i] + dfs(i+1, l)
val ans = maxOf(left, right)
memo[i][l] = ans
return ans
}
return dfs(0, 0)
}
}
Python
class Solution:
def maximumScore(self, nums: list[int], multipliers: list[int]) -> int:
n, m = len(nums), len(multipliers)
memo: dict[tuple[int, int], int] = {}
def dfs(i: int, l: int) -> int:
if i == m:
return 0
if (i, l) in memo:
return memo[(i, l)]
r = n - 1 - (i - l)
left = nums[l] * multipliers[i] + dfs(i+1, l+1)
right = nums[r] * multipliers[i] + dfs(i+1, l)
memo[(i, l)] = max(left, right)
return memo[(i, l)]
return dfs(0, 0)
Rust
impl Solution {
pub fn maximum_score(nums: Vec<i32>, multipliers: Vec<i32>) -> i32 {
let n = nums.len();
let m = multipliers.len();
let mut memo = vec![vec![None; m+1]; m+1];
fn dfs(i: usize, l: usize, nums: &Vec<i32>, multipliers: &Vec<i32>, memo: &mut Vec<Vec<Option<i32>>>) -> i32 {
if i == multipliers.len() { return 0; }
if let Some(val) = memo[i][l] { return val; }
let r = nums.len() - 1 - (i - l);
let left = nums[l] * multipliers[i] + dfs(i+1, l+1, nums, multipliers, memo);
let right = nums[r] * multipliers[i] + dfs(i+1, l, nums, multipliers, memo);
let ans = left.max(right);
memo[i][l] = Some(ans);
ans
}
dfs(0, 0, &nums, &multipliers, &mut memo)
}
}
TypeScript
class Solution {
maximumScore(nums: number[], multipliers: number[]): number {
const n = nums.length, m = multipliers.length;
const memo: (number | undefined)[][] = Array.from({length: m+1}, () => Array(m+1));
function dfs(i: number, l: number): number {
if (i === m) return 0;
if (memo[i][l] !== undefined) return memo[i][l]!;
const r = n - 1 - (i - l);
const left = nums[l] * multipliers[i] + dfs(i+1, l+1);
const right = nums[r] * multipliers[i] + dfs(i+1, l);
memo[i][l] = Math.max(left, right);
return memo[i][l]!;
}
return dfs(0, 0);
}
}
Complexity
- ⏰ Time complexity:
O(m^2)— There aremoperations and for each, up tompossible left picks, so the DP table ism x m. - 🧺 Space complexity:
O(m^2)— The memoization table stores results for each(i, l)pair.
Method 3 - Bottom Up DP
Intuition
This method uses bottom-up dynamic programming to avoid recursion and stack overhead. By iteratively filling a table, we build up solutions for smaller subproblems and use them to solve larger ones. The key is that the optimal score for each state depends only on the choices made so far, and we can compute these efficiently in reverse order.
Approach
- Create a 2D DP table
dpwheredp[i][left]stores the maximum score afterioperations andleftpicks from the left. - Iterate backwards over the number of operations (
ifromm-1to0). - For each possible number of left picks (
leftfrom0toi):- Compute the right index as
right = n - 1 - (i - left). - Update
memo[i][left]as the maximum of:- Picking from the left:
nums[left] * multipliers[i] + memo[i+1][left+1] - Picking from the right:
nums[right] * multipliers[i] + memo[i+1][left]
- Picking from the left:
- Compute the right index as
- The answer is stored in
memo[0][0]. - For space optimization, use a 1D array and update in place.
Code
C++
class Solution {
public:
int maximumScore(vector<int>& nums, vector<int>& multipliers) {
int n = nums.size(), m = multipliers.size();
vector<vector<int>> dp(m+1, vector<int>(m+1, 0));
for (int i = m-1; i >= 0; --i) {
for (int left = i; left >= 0; --left) {
int right = n - 1 - (i - left);
int pickLeft = nums[left] * multipliers[i] + dp[i+1][left+1];
int pickRight = nums[right] * multipliers[i] + dp[i+1][left];
dp[i][left] = max(pickLeft, pickRight);
}
}
return dp[0][0];
}
};
Go
func maximumScore(nums []int, multipliers []int) int {
n, m := len(nums), len(multipliers)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, m+1)
}
for i := m-1; i >= 0; i-- {
for left := i; left >= 0; left-- {
right := n - 1 - (i - left)
pickLeft := nums[left]*multipliers[i] + dp[i+1][left+1]
pickRight := nums[right]*multipliers[i] + dp[i+1][left]
if pickLeft > pickRight {
dp[i][left] = pickLeft
} else {
dp[i][left] = pickRight
}
}
}
return dp[0][0]
}
Java
class Solution {
public int maximumScore(int[] nums, int[] multipliers) {
int n = nums.length, m = multipliers.length;
int[][] dp = new int[m+1][m+1];
for (int i = m-1; i >= 0; i--) {
for (int left = i; left >= 0; left--) {
int right = n - 1 - (i - left);
int pickLeft = nums[left] * multipliers[i] + dp[i+1][left+1];
int pickRight = nums[right] * multipliers[i] + dp[i+1][left];
dp[i][left] = Math.max(pickLeft, pickRight);
}
}
return dp[0][0];
}
}
Kotlin
class Solution {
fun maximumScore(nums: IntArray, multipliers: IntArray): Int {
val n = nums.size
val m = multipliers.size
val dp = Array(m+1) { IntArray(m+1) }
for (i in m-1 downTo 0) {
for (left in i downTo 0) {
val right = n - 1 - (i - left)
val pickLeft = nums[left] * multipliers[i] + dp[i+1][left+1]
val pickRight = nums[right] * multipliers[i] + dp[i+1][left]
dp[i][left] = maxOf(pickLeft, pickRight)
}
}
return dp[0][0]
}
}
Python
class Solution:
def maximumScore(self, nums: list[int], multipliers: list[int]) -> int:
n, m = len(nums), len(multipliers)
dp = [[0] * (m+1) for _ in range(m+1)]
for i in range(m-1, -1, -1):
for left in range(i, -1, -1):
right = n - 1 - (i - left)
pick_left = nums[left] * multipliers[i] + dp[i+1][left+1]
pick_right = nums[right] * multipliers[i] + dp[i+1][left]
dp[i][left] = max(pick_left, pick_right)
return dp[0][0]
Rust
impl Solution {
pub fn maximum_score(nums: Vec<i32>, multipliers: Vec<i32>) -> i32 {
let n = nums.len();
let m = multipliers.len();
let mut dp = vec![vec![0; m+1]; m+1];
for i in (0..m).rev() {
for left in (0..=i).rev() {
let right = n - 1 - (i - left);
let pick_left = nums[left] * multipliers[i] + dp[i+1][left+1];
let pick_right = nums[right] * multipliers[i] + dp[i+1][left];
dp[i][left] = pick_left.max(pick_right);
}
}
dp[0][0]
}
}
TypeScript
class Solution {
maximumScore(nums: number[], multipliers: number[]): number {
const n = nums.length, m = multipliers.length;
const dp: number[][] = Array.from({length: m+1}, () => Array(m+1).fill(0));
for (let i = m-1; i >= 0; i--) {
for (let left = i; left >= 0; left--) {
const right = n - 1 - (i - left);
const pickLeft = nums[left] * multipliers[i] + dp[i+1][left+1];
const pickRight = nums[right] * multipliers[i] + dp[i+1][left];
dp[i][left] = Math.max(pickLeft, pickRight);
}
}
return dp[0][0];
}
}
Complexity
- ⏰ Time complexity:
O(m^2)— We fill a table of sizem x m. - 🧺 Space complexity:
O(m^2)— The memoization table ism x m. With space optimization, it can be reduced toO(m).
