3040. Maximum Number of Operations With the Same Score II
Problem Description
You are given an array of integers called nums
. You can perform operations on this array as long as it contains at least 2 elements. Each operation consists of selecting and deleting exactly 2 elements from the array in one of these three ways:
- Delete the first two elements of
nums
- Delete the last two elements of
nums
- Delete the first and the last elements of
nums
The score of each operation is the sum of the two deleted elements.
The key constraint is that all operations must have the same score. For example, if your first operation has a score of 5, then every subsequent operation must also have a score of 5.
Your task is to find the maximum number of operations you can perform while maintaining this same-score constraint.
For example, if nums = [3, 2, 1, 4, 5]
, you might be able to perform operations where each has a score of 5:
- Delete first and last (3 + 5 = 8) - one possible score
- Delete first two (3 + 2 = 5) - another possible score
- Delete last two (4 + 5 = 9) - yet another possible score
You need to pick one target score and maximize the number of operations with that score. The function should return the maximum possible number of operations.
Intuition
The first key observation is that once we perform the first operation, the target score s
is fixed. Since we have three possible ways to perform the first operation, we only have three possible values for the target score:
s = nums[0] + nums[1]
(delete first two elements)s = nums[0] + nums[n-1]
(delete first and last elements)s = nums[n-1] + nums[n-2]
(delete last two elements)
Once we fix the target score s
and perform the first operation, we're left with a subarray and need to find the maximum number of operations we can perform on this subarray with the same score s
.
This naturally leads to a recursive approach. After removing two elements, we get a smaller array, and we face the same problem: how many operations can we perform on this smaller array with score s
?
The state of our problem can be represented by the current boundaries of the array (indices i
and j
), and we want to find the maximum operations possible in the range [i, j]
with a fixed score s
.
At each step, we have up to three choices (depending on whether they give us the target score s
):
- Take the first two elements if
nums[i] + nums[i+1] == s
- Take the first and last elements if
nums[i] + nums[j] == s
- Take the last two elements if
nums[j-1] + nums[j] == s
We try all valid choices and recursively solve for the remaining subarray, keeping track of the maximum operations possible.
Since we might encounter the same subarray boundaries multiple times during recursion, we use memoization to avoid redundant calculations. The @cache
decorator stores the results of dfs(i, j, s)
to reuse them when needed.
The final answer is 1
(for the initial operation that fixes the score) plus the maximum operations we can perform on the remaining array for each of the three possible initial scores.
Learn more about Memoization and Dynamic Programming patterns.
Solution Approach
The solution uses memoization search (dynamic programming with recursion) to efficiently explore all possible operation sequences.
Implementation Details
-
Define the recursive function
dfs(i, j, s)
:- Parameters:
i
: left boundary of the current subarrayj
: right boundary of the current subarray (inclusive)s
: the target score that all operations must match
- Returns: maximum number of operations possible in the range
[i, j]
with scores
- Parameters:
-
Base case:
- If
j - i < 1
, the subarray has fewer than 2 elements, so no operations are possible. Return0
.
- If
-
Recursive cases:
- Initialize
ans = 0
to track the maximum operations - Option 1: Delete first two elements
- Check if
nums[i] + nums[i+1] == s
- If valid, recursively solve for
[i+2, j]
and update:ans = max(ans, 1 + dfs(i+2, j, s))
- Check if
- Option 2: Delete first and last elements
- Check if
nums[i] + nums[j] == s
- If valid, recursively solve for
[i+1, j-1]
and update:ans = max(ans, 1 + dfs(i+1, j-1, s))
- Check if
- Option 3: Delete last two elements
- Check if
nums[j-1] + nums[j] == s
- If valid, recursively solve for
[i, j-2]
and update:ans = max(ans, 1 + dfs(i, j-2, s))
- Check if
- Initialize
-
- The
@cache
decorator automatically stores results ofdfs(i, j, s)
to avoid recomputing the same subproblems
- The
-
Main logic:
- Calculate the three possible initial scores:
s1 = nums[0] + nums[1]
→ After this operation, solvedfs(2, n-1, s1)
s2 = nums[n-1] + nums[n-2]
→ After this operation, solvedfs(0, n-3, s2)
s3 = nums[0] + nums[n-1]
→ After this operation, solvedfs(1, n-2, s3)
- Return
1 + max(a, b, c)
where:- The
1
accounts for the initial operation a
,b
,c
are the maximum operations for each initial choice
- The
- Calculate the three possible initial scores:
Time and Space Complexity
- Time Complexity:
O(n²)
for each of the three initial scores, soO(n²)
overall, wheren
is the length of the array. Each subproblem(i, j)
is computed at most once due to memoization. - Space Complexity:
O(n²)
for the memoization cache storing results for all possible(i, j)
pairs.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [3, 2, 6, 1, 4]
.
Step 1: Identify possible initial scores
We have three options for the first operation:
- Option 1: Delete first two (3, 2) → score = 5
- Option 2: Delete first and last (3, 4) → score = 7
- Option 3: Delete last two (1, 4) → score = 5
Step 2: Explore each initial choice
Let's trace Option 1 (score = 5):
- Initial operation: Delete (3, 2), remaining array = [6, 1, 4]
- Now we call
dfs(2, 4, 5)
where indices 2-4 represent [6, 1, 4] - In this recursive call, we check:
- Can we delete first two? 6 + 1 = 7 ≠ 5 ❌
- Can we delete first and last? 6 + 4 = 10 ≠ 5 ❌
- Can we delete last two? 1 + 4 = 5 ✓
- We take the last two, getting 1 +
dfs(2, 2, 5)
dfs(2, 2, 5)
has only 1 element [6], so it returns 0- Total for Option 1: 1 (initial) + 1 (recursive) = 2 operations
Let's trace Option 2 (score = 7):
- Initial operation: Delete (3, 4), remaining array = [2, 6, 1]
- Now we call
dfs(1, 3, 7)
where indices 1-3 represent [2, 6, 1] - In this recursive call, we check:
- Can we delete first two? 2 + 6 = 8 ≠ 7 ❌
- Can we delete first and last? 2 + 1 = 3 ≠ 7 ❌
- Can we delete last two? 6 + 1 = 7 ✓
- We take the last two, getting 1 +
dfs(1, 1, 7)
dfs(1, 1, 7)
has only 1 element [2], so it returns 0- Total for Option 2: 1 (initial) + 1 (recursive) = 2 operations
Let's trace Option 3 (score = 5):
- Initial operation: Delete (1, 4), remaining array = [3, 2, 6]
- Now we call
dfs(0, 2, 5)
where indices 0-2 represent [3, 2, 6] - In this recursive call, we check:
- Can we delete first two? 3 + 2 = 5 ✓
- Can we delete first and last? 3 + 6 = 9 ≠ 5 ❌
- Can we delete last two? 2 + 6 = 8 ≠ 5 ❌
- We take the first two, getting 1 +
dfs(2, 2, 5)
dfs(2, 2, 5)
has only 1 element [6], so it returns 0- Total for Option 3: 1 (initial) + 1 (recursive) = 2 operations
Step 3: Return the maximum
All three options yield 2 operations, so the answer is 2.
Note how memoization helps: if we had called dfs(2, 2, 5)
multiple times (which can happen in larger examples), we'd only compute it once and reuse the cached result.
Solution Implementation
1class Solution:
2 def maxOperations(self, nums: List[int]) -> int:
3 """
4 Find the maximum number of operations where each operation removes
5 two elements with the same sum from the array.
6
7 Args:
8 nums: List of integers
9
10 Returns:
11 Maximum number of valid operations
12 """
13 from functools import cache
14
15 @cache
16 def dfs(left: int, right: int, target_sum: int) -> int:
17 """
18 Recursively find maximum operations in range [left, right] with target sum.
19
20 Args:
21 left: Left boundary index (inclusive)
22 right: Right boundary index (inclusive)
23 target_sum: Required sum for each operation
24
25 Returns:
26 Maximum number of operations possible
27 """
28 # Base case: need at least 2 elements for an operation
29 if right - left < 1:
30 return 0
31
32 max_operations = 0
33
34 # Option 1: Remove two leftmost elements
35 if nums[left] + nums[left + 1] == target_sum:
36 max_operations = max(max_operations, 1 + dfs(left + 2, right, target_sum))
37
38 # Option 2: Remove leftmost and rightmost elements
39 if nums[left] + nums[right] == target_sum:
40 max_operations = max(max_operations, 1 + dfs(left + 1, right - 1, target_sum))
41
42 # Option 3: Remove two rightmost elements
43 if nums[right - 1] + nums[right] == target_sum:
44 max_operations = max(max_operations, 1 + dfs(left, right - 2, target_sum))
45
46 return max_operations
47
48 n = len(nums)
49
50 # Try three possible initial operations to establish the target sum
51 # Each branch commits to a different first operation
52
53 # Branch 1: First operation uses two leftmost elements
54 result_left = dfs(2, n - 1, nums[0] + nums[1])
55
56 # Branch 2: First operation uses two rightmost elements
57 result_right = dfs(0, n - 3, nums[-1] + nums[-2])
58
59 # Branch 3: First operation uses leftmost and rightmost elements
60 result_ends = dfs(1, n - 2, nums[0] + nums[-1])
61
62 # Return 1 (for initial operation) plus maximum from remaining operations
63 return 1 + max(result_left, result_right, result_ends)
64
1class Solution {
2 // Memoization table for dynamic programming
3 private Integer[][] memo;
4 // Input array
5 private int[] nums;
6 // Target sum for each operation
7 private int targetSum;
8 // Length of the input array
9 private int n;
10
11 /**
12 * Finds the maximum number of operations that can be performed on the array.
13 * An operation consists of removing two elements that sum to a constant value.
14 *
15 * @param nums The input array
16 * @return Maximum number of operations possible
17 */
18 public int maxOperations(int[] nums) {
19 this.nums = nums;
20 n = nums.length;
21
22 // Try three different initial operations and their corresponding target sums
23 // Option 1: Remove first two elements
24 int option1 = calculateMaxOperations(2, n - 1, nums[0] + nums[1]);
25
26 // Option 2: Remove last two elements
27 int option2 = calculateMaxOperations(0, n - 3, nums[n - 2] + nums[n - 1]);
28
29 // Option 3: Remove first and last elements
30 int option3 = calculateMaxOperations(1, n - 2, nums[0] + nums[n - 1]);
31
32 // Return 1 (for the initial operation) plus the maximum of the three options
33 return 1 + Math.max(option1, Math.max(option2, option3));
34 }
35
36 /**
37 * Helper method to initialize memoization and start DFS with given parameters.
38 *
39 * @param start Starting index of the remaining subarray
40 * @param end Ending index of the remaining subarray
41 * @param sum Target sum for all operations
42 * @return Maximum number of operations for the given configuration
43 */
44 private int calculateMaxOperations(int start, int end, int sum) {
45 // Initialize new memoization table for this configuration
46 memo = new Integer[n][n];
47 this.targetSum = sum;
48 return dfs(start, end);
49 }
50
51 /**
52 * Depth-first search with memoization to find maximum operations.
53 *
54 * @param left Left boundary of current subarray (inclusive)
55 * @param right Right boundary of current subarray (inclusive)
56 * @return Maximum number of operations possible in the current subarray
57 */
58 private int dfs(int left, int right) {
59 // Base case: need at least 2 elements for an operation
60 if (right - left < 1) {
61 return 0;
62 }
63
64 // Check memoization table
65 if (memo[left][right] != null) {
66 return memo[left][right];
67 }
68
69 int maxOperations = 0;
70
71 // Option 1: Remove the leftmost two elements
72 if (nums[left] + nums[left + 1] == targetSum) {
73 maxOperations = Math.max(maxOperations, 1 + dfs(left + 2, right));
74 }
75
76 // Option 2: Remove the leftmost and rightmost elements
77 if (nums[left] + nums[right] == targetSum) {
78 maxOperations = Math.max(maxOperations, 1 + dfs(left + 1, right - 1));
79 }
80
81 // Option 3: Remove the rightmost two elements
82 if (nums[right - 1] + nums[right] == targetSum) {
83 maxOperations = Math.max(maxOperations, 1 + dfs(left, right - 2));
84 }
85
86 // Store result in memoization table and return
87 return memo[left][right] = maxOperations;
88 }
89}
90
1class Solution {
2public:
3 int maxOperations(vector<int>& nums) {
4 int n = nums.size();
5
6 // Lambda function to calculate maximum operations for a given range and target sum
7 auto calculateMaxOperations = [&](int start, int end, int targetSum) -> int {
8 // dp[i][j] stores the maximum operations possible for subarray from index i to j
9 vector<vector<int>> dp(n, vector<int>(n, -1));
10
11 // DFS with memoization to find maximum operations
12 function<int(int, int)> dfs = [&](int left, int right) -> int {
13 // Base case: if less than 2 elements remain, no operation possible
14 if (right - left < 1) {
15 return 0;
16 }
17
18 // Return memoized result if already computed
19 if (dp[left][right] != -1) {
20 return dp[left][right];
21 }
22
23 int maxOps = 0;
24
25 // Option 1: Remove first two elements if their sum equals target
26 if (nums[left] + nums[left + 1] == targetSum) {
27 maxOps = max(maxOps, 1 + dfs(left + 2, right));
28 }
29
30 // Option 2: Remove first and last elements if their sum equals target
31 if (nums[left] + nums[right] == targetSum) {
32 maxOps = max(maxOps, 1 + dfs(left + 1, right - 1));
33 }
34
35 // Option 3: Remove last two elements if their sum equals target
36 if (nums[right - 1] + nums[right] == targetSum) {
37 maxOps = max(maxOps, 1 + dfs(left, right - 2));
38 }
39
40 // Store and return the result
41 return dp[left][right] = maxOps;
42 };
43
44 return dfs(start, end);
45 };
46
47 // Try three possible initial operations and find the maximum
48 // Case 1: Start by removing first two elements
49 int caseFirst = calculateMaxOperations(2, n - 1, nums[0] + nums[1]);
50
51 // Case 2: Start by removing last two elements
52 int caseLast = calculateMaxOperations(0, n - 3, nums[n - 2] + nums[n - 1]);
53
54 // Case 3: Start by removing first and last elements
55 int caseFirstLast = calculateMaxOperations(1, n - 2, nums[0] + nums[n - 1]);
56
57 // Return 1 (for the initial operation) plus the maximum from remaining operations
58 return 1 + max({caseFirst, caseLast, caseFirstLast});
59 }
60};
61
1/**
2 * Finds the maximum number of operations that can be performed on an array
3 * where each operation removes two elements that sum to a specific value
4 * @param nums - The input array of numbers
5 * @returns The maximum number of operations possible
6 */
7function maxOperations(nums: number[]): number {
8 const arrayLength: number = nums.length;
9
10 /**
11 * Helper function to calculate maximum operations for a given range and target sum
12 * @param startIndex - Starting index of the range
13 * @param endIndex - Ending index of the range (inclusive)
14 * @param targetSum - The target sum for valid operations
15 * @returns Maximum number of operations possible in the given range
16 */
17 const calculateMaxOperationsInRange = (startIndex: number, endIndex: number, targetSum: number): number => {
18 // Initialize memoization table for dynamic programming
19 const memoTable: number[][] = Array.from(
20 { length: arrayLength },
21 () => Array(arrayLength).fill(-1)
22 );
23
24 /**
25 * Recursive function with memoization to find maximum operations
26 * @param left - Current left boundary of the subarray
27 * @param right - Current right boundary of the subarray (inclusive)
28 * @returns Maximum operations possible in the current subarray
29 */
30 const depthFirstSearch = (left: number, right: number): number => {
31 // Base case: less than 2 elements remaining
32 if (right - left < 1) {
33 return 0;
34 }
35
36 // Return memoized result if already computed
37 if (memoTable[left][right] !== -1) {
38 return memoTable[left][right];
39 }
40
41 let maxOperationsCount: number = 0;
42
43 // Option 1: Remove first two elements if they sum to target
44 if (nums[left] + nums[left + 1] === targetSum) {
45 maxOperationsCount = Math.max(
46 maxOperationsCount,
47 1 + depthFirstSearch(left + 2, right)
48 );
49 }
50
51 // Option 2: Remove first and last elements if they sum to target
52 if (nums[left] + nums[right] === targetSum) {
53 maxOperationsCount = Math.max(
54 maxOperationsCount,
55 1 + depthFirstSearch(left + 1, right - 1)
56 );
57 }
58
59 // Option 3: Remove last two elements if they sum to target
60 if (nums[right - 1] + nums[right] === targetSum) {
61 maxOperationsCount = Math.max(
62 maxOperationsCount,
63 1 + depthFirstSearch(left, right - 2)
64 );
65 }
66
67 // Store and return the computed result
68 memoTable[left][right] = maxOperationsCount;
69 return maxOperationsCount;
70 };
71
72 return depthFirstSearch(startIndex, endIndex);
73 };
74
75 // Try three different initial operations and find the maximum
76 // Case 1: Start by removing first two elements
77 const caseRemoveFirstTwo: number = calculateMaxOperationsInRange(
78 2,
79 arrayLength - 1,
80 nums[0] + nums[1]
81 );
82
83 // Case 2: Start by removing last two elements
84 const caseRemoveLastTwo: number = calculateMaxOperationsInRange(
85 0,
86 arrayLength - 3,
87 nums[arrayLength - 2] + nums[arrayLength - 1]
88 );
89
90 // Case 3: Start by removing first and last elements
91 const caseRemoveFirstAndLast: number = calculateMaxOperationsInRange(
92 1,
93 arrayLength - 2,
94 nums[0] + nums[arrayLength - 1]
95 );
96
97 // Return 1 (for the initial operation) plus the maximum from the three cases
98 return 1 + Math.max(caseRemoveFirstTwo, caseRemoveLastTwo, caseRemoveFirstAndLast);
99}
100
Time and Space Complexity
The time complexity is O(n^2)
, and the space complexity is O(n^2)
. Here, n
is the length of the array nums
.
Time Complexity Analysis:
- The recursive function
dfs(i, j, s)
uses memoization with@cache
decorator - The state space is defined by parameters
(i, j, s)
, wherei
andj
are indices ranging from0
ton-1
- The parameter
s
can only take at most 3 distinct values (corresponding tonums[0] + nums[1]
,nums[-1] + nums[-2]
, ornums[0] + nums[-1]
) - For each unique combination of
(i, j, s)
, there areO(n^2)
possible states sincei
andj
each range from0
ton-1
- Each state is computed at most once due to memoization
- Within each recursive call, the function performs constant time operations
O(1)
- just comparisons and at most 3 recursive calls - Therefore, the overall time complexity is
O(n^2) * O(1) = O(n^2)
Space Complexity Analysis:
- The memoization cache stores results for all unique
(i, j, s)
combinations - As analyzed above, there are
O(n^2)
possible states that can be cached - The recursion depth in the worst case is
O(n)
when we reduce the array by 2 elements at each step - Therefore, the space complexity is
O(n^2)
for the cache plusO(n)
for the recursion stack, resulting inO(n^2)
overall
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Considering All Three Initial Choices
Pitfall: A common mistake is to only try one or two of the possible initial operations, missing potential optimal solutions.
# WRONG: Only considering one initial choice
def maxOperations(self, nums: List[int]) -> int:
@cache
def dfs(left, right, target_sum):
# ... implementation ...
# Only trying the first two elements as initial operation
return 1 + dfs(2, len(nums) - 1, nums[0] + nums[1])
Solution: Always explore all three possible initial operations since each establishes a different target sum that might lead to the maximum number of operations:
# CORRECT: Consider all three initial choices
result_left = dfs(2, n - 1, nums[0] + nums[1])
result_right = dfs(0, n - 3, nums[-1] + nums[-2])
result_ends = dfs(1, n - 2, nums[0] + nums[-1])
return 1 + max(result_left, result_right, result_ends)
2. Incorrect Index Bounds After Operations
Pitfall: Using wrong indices when updating the boundaries after an operation, especially for the "delete last two" case.
# WRONG: Incorrect boundary update for deleting last two elements
if nums[right - 1] + nums[right] == target_sum:
max_operations = max(max_operations, 1 + dfs(left, right - 1, target_sum)) # Should be right - 2!
Solution: Carefully track index changes:
- Delete first two:
[left, right]
→[left + 2, right]
- Delete first and last:
[left, right]
→[left + 1, right - 1]
- Delete last two:
[left, right]
→[left, right - 2]
3. Forgetting to Add 1 for the Initial Operation
Pitfall: The recursive function dfs
only counts operations after the initial one. Forgetting to add 1 for the initial operation leads to an off-by-one error.
# WRONG: Missing the initial operation count
return max(result_left, result_right, result_ends) # Should be 1 + max(...)
Solution: Remember that each of the three branches performs one initial operation to establish the target sum, then recursively finds additional operations:
# CORRECT: Account for the initial operation
return 1 + max(result_left, result_right, result_ends)
4. Not Handling Edge Cases Properly
Pitfall: Not checking if the array has enough elements for the initial operations.
# WRONG: Accessing indices without bounds checking
def maxOperations(self, nums: List[int]) -> int:
n = len(nums)
# Might cause IndexError if n < 2
result_right = dfs(0, n - 3, nums[-1] + nums[-2]) # n - 3 could be negative!
Solution: Add validation for minimum array size:
# CORRECT: Handle edge cases
def maxOperations(self, nums: List[int]) -> int:
n = len(nums)
if n < 2:
return 0 # Can't perform any operations
# Safe to proceed with all three initial choices
# ...
5. Cache Invalidation Between Different Initial Choices
Pitfall: Using a global cache that doesn't account for different target sums can lead to incorrect results if not handled properly.
Solution: The provided solution correctly handles this by including the target sum s
as a parameter in the memoized function dfs(i, j, s)
. This ensures that different target sums maintain separate cache entries, preventing conflicts between the three different initial choice branches.
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
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
Want a Structured Path to Master System Design Too? Don’t Miss This!