Facebook Pixel

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:

  1. Delete the first two elements of nums
  2. Delete the last two elements of nums
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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):

  1. Take the first two elements if nums[i] + nums[i+1] == s
  2. Take the first and last elements if nums[i] + nums[j] == s
  3. 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

  1. Define the recursive function dfs(i, j, s):

    • Parameters:
      • i: left boundary of the current subarray
      • j: 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 score s
  2. Base case:

    • If j - i < 1, the subarray has fewer than 2 elements, so no operations are possible. Return 0.
  3. 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))
    • 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))
    • 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))
  4. Memoization:

    • The @cache decorator automatically stores results of dfs(i, j, s) to avoid recomputing the same subproblems
  5. Main logic:

    • Calculate the three possible initial scores:
      • s1 = nums[0] + nums[1] → After this operation, solve dfs(2, n-1, s1)
      • s2 = nums[n-1] + nums[n-2] → After this operation, solve dfs(0, n-3, s2)
      • s3 = nums[0] + nums[n-1] → After this operation, solve dfs(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

Time and Space Complexity

  • Time Complexity: O(n²) for each of the three initial scores, so O(n²) overall, where n 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 Evaluator

Example 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), where i and j are indices ranging from 0 to n-1
  • The parameter s can only take at most 3 distinct values (corresponding to nums[0] + nums[1], nums[-1] + nums[-2], or nums[0] + nums[-1])
  • For each unique combination of (i, j, s), there are O(n^2) possible states since i and j each range from 0 to n-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 plus O(n) for the recursion stack, resulting in O(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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of these pictures shows the visit order of a depth-first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More