Maximum Number of Operations With the Same Score II
MedPrompt
Given an array of integers called nums, you can perform any of the following operation while nums contains at least 2 elements:
- Choose the first two elements of
numsand delete them. - Choose the last two elements of
numsand delete them. - Choose the first and the last elements of
numsand delete them.
The score of the operation is the sum of the deleted elements.
Your task is to find the maximum number of operations that can be performed, such that all operations have the same score.
Return the maximum number of operations possible that satisfy the condition mentioned above.
Example 1:
Input: nums = [3,2,1,2,3,4]
Output: 3
Explanation: We perform the following operations:
- Delete the first two elements, with score 3 + 2 = 5, nums = [1,2,3,4].
- Delete the first and the last elements, with score 1 + 4 = 5, nums = [2,3].
- Delete the first and the last elements, with score 2 + 3 = 5, nums = [].
We are unable to perform any more operations as nums is empty.Example 2:
Input: nums = [3,2,6,1,4]
Output: 2
Explanation: We perform the following operations:
- Delete the first two elements, with score 3 + 2 = 5, nums = [6,1,4].
- Delete the last two elements, with score 1 + 4 = 5, nums = [6].
It can be proven that we can perform at most 2 operations.
Constraints:
2 <= nums.length <= 20001 <= nums[i] <= 1000
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly translates the problem's recursive nature into code. We identify that the first operation determines the target score for all subsequent operations. There are three possible first moves, leading to three potential target scores. For each target score, we can define a recursive function that explores all possible sequences of operations. The state of the recursion can be defined by the start and end indices of the current subarray. The function tries all three types of operations (from the front, from the back, from both ends) and recursively calls itself on the smaller subarray, taking the path that yields the maximum number of operations.
Algorithm
- The problem can be broken down into three main scenarios based on the first operation:
a. Removing the first two elements (
nums[0],nums[1]) b. Removing the last two elements (nums[n-1],nums[n-2]) c. Removing the first and last elements (nums[0],nums[n-1]) - The sum of the elements removed in the first operation becomes the target
scorefor all subsequent operations. - We define a recursive helper function, say
solve(start, end, score), which calculates the maximum number of operations for the subarraynums[start...end]with the givenscore. - Inside
solve(start, end, score):- Base Case: If
start >= end(less than two elements left), return 0. - Recursive Step: Explore three possible moves:
- If
nums[start] + nums[start+1] == score, recursively call1 + solve(start+2, end, score). - If
nums[end] + nums[end-1] == score, recursively call1 + solve(start, end-2, score). - If
nums[start] + nums[end] == score, recursively call1 + solve(start+1, end-1, score).
- If
- Base Case: If
- The result for the current state is the maximum of the outcomes of the valid moves. If no move is valid, return 0.
- The final answer is the maximum result obtained from the three initial scenarios.
Walkthrough
The main function maxOperations considers three initial scenarios:
- Removing
nums[0]andnums[1]. The score isnums[0] + nums[1]. We get 1 operation plus whatever we can get from the rest of the arraynums[2...n-1]. - Removing
nums[n-2]andnums[n-1]. The score isnums[n-2] + nums[n-1]. We get 1 operation plus the result fromnums[0...n-3]. - Removing
nums[0]andnums[n-1]. The score isnums[0] + nums[n-1]. We get 1 operation plus the result fromnums[1...n-2].
A helper function solve(nums, start, end, score) is implemented to handle the recursive calculation. It returns the maximum number of operations possible on the subarray nums[start...end] for a fixed score. In each call, it checks all three possible moves. If a move's sum matches the target score, it makes a recursive call for the resulting subproblem and adds 1 (for the current operation). The function returns the maximum value among all valid moves.
class Solution { public int maxOperations(int[] nums) { int n = nums.length; if (n < 2) { return 0; } int maxOps = 0; // Case 1: Remove first two elements maxOps = Math.max(maxOps, 1 + solve(nums, 2, n - 1, nums[0] + nums[1])); // Case 2: Remove last two elements maxOps = Math.max(maxOps, 1 + solve(nums, 0, n - 3, nums[n - 1] + nums[n - 2])); // Case 3: Remove first and last elements maxOps = Math.max(maxOps, 1 + solve(nums, 1, n - 2, nums[0] + nums[n - 1])); return maxOps; } private int solve(int[] nums, int start, int end, int score) { if (start >= end) { return 0; } int res = 0; // Option 1: remove from front if (nums[start] + nums[start + 1] == score) { res = Math.max(res, 1 + solve(nums, start + 2, end, score)); } // Option 2: remove from back if (nums[end] + nums[end - 1] == score) { res = Math.max(res, 1 + solve(nums, start, end - 2, score)); } // Option 3: remove from both ends if (nums[start] + nums[end] == score) { res = Math.max(res, 1 + solve(nums, start + 1, end - 1, score)); } return res; }}Complexity
Time
O(3^(n/2)) - In the worst-case scenario, each recursive call can branch into three further calls. The depth of the recursion is roughly `n/2`. This leads to an exponential number of function calls, making it too slow for the given constraints.
Space
O(n) - The space complexity is determined by the maximum depth of the recursion stack, which can be up to `n/2` in the worst case.
Trade-offs
Pros
Simple to conceptualize and implement.
Directly models the decision-making process described in the problem.
Cons
Highly inefficient due to the re-computation of the same subproblems.
Will result in a 'Time Limit Exceeded' (TLE) error for the given constraints.
The brute-force recursive approach is inefficient because it repeatedly solves the same subproblems. For a given target score, the maximum number of operations for a subarray nums[start...end] is always the same, yet the naive recursion calculates it multiple times. We can significantly optimize this by using memoization, a top-down dynamic programming technique. We store the result of each subproblem (start, end) in a 2D cache (or memoization table). When the function is called for a subproblem, it first checks the cache. If the result is present, it's returned instantly, avoiding redundant computation. If not, the result is computed, stored in the cache, and then returned.
Algorithm
- The core recursive structure remains the same as the brute-force approach. The state is defined by
(start, end). - We introduce a 2D array,
memo[n][n], to store the results of thesolve(start, end)function for a fixed score. A separate memoization table is used for each of the three initial score possibilities. - The helper function is modified to
solve(start, end, score, memo). - Before computing the result for a state
(start, end), we first check ifmemo[start][end]has already been computed (e.g., it's notnull). If so, we return the stored value immediately. - If the result is not in the memoization table, we compute it recursively as in the brute-force approach.
- After computing the result, we store it in
memo[start][end]before returning it. - The main function initializes three separate memoization tables and calls the helper function for each of the three initial scenarios.
Walkthrough
This approach enhances the recursive solution by adding a memoization table, typically a 2D array memo, to store the results of subproblems. The state (start, end) maps to memo[start][end]. We need a separate memoization context for each of the three initial scores, so we'll call a helper function three times, each with its own memoization table.
The helper function solve(nums, start, end, score, memo) first checks if memo[start][end] contains a pre-computed result. If it does, it returns that value. Otherwise, it calculates the result by exploring the three possible moves, just like in the brute-force version. The key difference is that before returning the calculated result, it stores it in memo[start][end]. This ensures that for any given score, the subproblem for (start, end) is solved only once.
class Solution { public int maxOperations(int[] nums) { int n = nums.length; if (n < 2) { return 0; } int maxOps = 0; // Case 1: Remove first two elements int score1 = nums[0] + nums[1]; Integer[][] memo1 = new Integer[n][n]; maxOps = Math.max(maxOps, 1 + solve(nums, 2, n - 1, score1, memo1)); // Case 2: Remove last two elements int score2 = nums[n - 1] + nums[n - 2]; Integer[][] memo2 = new Integer[n][n]; maxOps = Math.max(maxOps, 1 + solve(nums, 0, n - 3, score2, memo2)); // Case 3: Remove first and last elements int score3 = nums[0] + nums[n - 1]; Integer[][] memo3 = new Integer[n][n]; maxOps = Math.max(maxOps, 1 + solve(nums, 1, n - 2, score3, memo3)); return maxOps; } private int solve(int[] nums, int start, int end, int score, Integer[][] memo) { if (start >= end) { return 0; } if (memo[start][end] != null) { return memo[start][end]; } int res = 0; // Option 1: remove from front if (nums[start] + nums[start + 1] == score) { res = Math.max(res, 1 + solve(nums, start + 2, end, score, memo)); } // Option 2: remove from back if (nums[end] + nums[end - 1] == score) { res = Math.max(res, 1 + solve(nums, start, end - 2, score, memo)); } // Option 3: remove from both ends if (nums[start] + nums[end] == score) { res = Math.max(res, 1 + solve(nums, start + 1, end - 1, score, memo)); } return memo[start][end] = res; }}Complexity
Time
O(n^2) - There are `O(n^2)` unique subproblems defined by `(start, end)`. Each subproblem is computed only once. The computation within each call is constant time. Since we run this process for three different initial scores, the total time complexity is `3 * O(n^2)`, which simplifies to `O(n^2)`.
Space
O(n^2) - The space is dominated by the `n x n` memoization table. The recursion stack depth contributes an additional O(n) space.
Trade-offs
Pros
Efficient and optimal for the given constraints.
Avoids re-computation of subproblems, drastically reducing the time complexity from exponential to polynomial.
It is a standard and powerful technique for problems with optimal substructure and overlapping subproblems.
Cons
Requires O(n^2) extra space for the memoization table, which might be a concern for extremely large constraints on
n.
Solutions
Solution
class Solution {private Integer[][] f;private int[] nums;private int s;private int n;public int maxOperations(int[] nums) { this.nums = nums; n = nums.length; int a = g(2, n - 1, nums[0] + nums[1]); int b = g(0, n - 3, nums[n - 2] + nums[n - 1]); int c = g(1, n - 2, nums[0] + nums[n - 1]); return 1 + Math.max(a, Math.max(b, c)); }private int g(int i, int j, int s) { f = new Integer[n][n]; this.s = s; return dfs(i, j); }private int dfs(int i, int j) { if (j - i < 1) { return 0; } if (f[i][j] != null) { return f[i][j]; } int ans = 0; if (nums[i] + nums[i + 1] == s) { ans = Math.max(ans, 1 + dfs(i + 2, j)); } if (nums[i] + nums[j] == s) { ans = Math.max(ans, 1 + dfs(i + 1, j - 1)); } if (nums[j - 1] + nums[j] == s) { ans = Math.max(ans, 1 + dfs(i, j - 2)); } return f[i][j] = ans; }}class Solution {public: int maxOperations(vector<int> &nums) { int n = nums.size(); int f[n][n]; auto g = [&](int i, int j, int s) -> int { memset(f, -1, sizeof(f)); function<int(int, int)> dfs = [&](int i, int j) -> int { if (j - i < 1) { return 0; } if (f[i][j] != -1) { return f[i][j]; } int ans = 0; if (nums[i] + nums[i + 1] == s) { ans = max(ans, 1 + dfs(i + 2, j)); } if (nums[i] + nums[j] == s) { ans = max(ans, 1 + dfs(i + 1, j - 1)); } if (nums[j - 1] + nums[j] == s) { ans = max(ans, 1 + dfs(i, j - 2)); } return f[i][j] = ans; }; return dfs(i, j); }; int a = g(2, n - 1, nums[0] + nums[1]); int b = g(0, n - 3, nums[n - 2] + nums[n - 1]); int c = g(1, n - 2, nums[0] + nums[n - 1]); return 1 + max({a, b, c}); }};class Solution: def maxOperations(self, nums: List[int]) -> int: @ cache def dfs(i: int, j: int, s: int) -> int: if j - i < 1: return 0 ans = 0 if nums[i] + nums[i + 1] == s: ans = max(ans, 1 + dfs(i + 2, j, s)) if nums[i] + nums[j] == s: ans = max(ans, 1 + dfs(i + 1, j - 1, s)) if nums[j - 1] + nums[j] == s: ans = max(ans, 1 + dfs(i, j - 2, s)) return ans n = len(nums) a = dfs(2, n - 1, nums[0] + nums[1]) b = dfs(0, n - 3, nums[- 1] + nums[- 2]) c = dfs(1, n - 2, nums[0] + nums[- 1]) return 1 + max(a, b, c)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.