Facebook Pixel

3018. Maximum Number of Removal Queries That Can Be Processed I 🔒

Problem Description

You have a 0-indexed array nums and a 0-indexed array queries that need to be processed.

Initial Operation (Optional):

  • Before processing any queries, you can perform at most one operation: replace nums with any subsequence of itself (keeping the relative order of selected elements).

Query Processing: Process each query in queries sequentially using these rules:

  • If both the first AND last elements of nums are less than queries[i], stop processing immediately.
  • Otherwise, you must remove either the first OR last element of nums, but only if that element is greater than or equal to queries[i].
  • Continue to the next query.

Goal: Find the maximum number of queries that can be processed by choosing the optimal initial subsequence and making optimal removal choices during query processing.

Example walkthrough: If you have nums = [3, 1, 4, 2] and queries = [2, 3, 1]:

  • You might choose to keep the subsequence [3, 4, 2] initially
  • For queries[0] = 2: Both first (3) and last (2) elements are ≥ 2, so you can remove one (say remove 2)
  • Now nums = [3, 4]
  • For queries[1] = 3: Both 3 and 4 are ≥ 3, so you can remove one (say remove 3)
  • Now nums = [4]
  • For queries[2] = 1: The element 4 is ≥ 1, so you can remove it
  • Successfully processed all 3 queries

The challenge is finding which initial subsequence to select and which elements to remove at each step to maximize the total number of queries processed.

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

Intuition

The key insight is that once we choose an initial subsequence, the query processing creates a pattern: we're removing elements from either end of the array, one at a time. This means we're essentially working with a contiguous subarray that shrinks from both ends.

Think about it backwards - if we successfully process k queries, we must have removed k elements from our initial subsequence. These removals happen from the ends, so we're left with some contiguous interval [i, j] of the original array that hasn't been touched yet.

This observation leads to a dynamic programming approach where f[i][j] represents the maximum number of queries we can process when we still have the interval [i, j] remaining. The beauty is that this implicitly handles the initial subsequence selection - by considering all possible intervals [i, j], we're effectively considering all possible subsequences that could remain after processing some queries.

For any interval [i, j], we can think about how we got here:

  • We might have just removed element at index i-1 (if it exists), meaning we came from state f[i-1][j]
  • We might have just removed element at index j+1 (if it exists), meaning we came from state f[i][j+1]

When transitioning from these states, we can process one more query if the element we're removing (nums[i-1] or nums[j+1]) is greater than or equal to the current query value queries[f[i-1][j]] or queries[f[i][j+1]].

The final answer considers all possible single elements that could be the last remaining element (represented by f[i][i]), plus one more query if that last element can handle it. This elegantly captures all possible ways to select an initial subsequence and process queries optimally.

Learn more about Dynamic Programming patterns.

Solution Approach

We implement a dynamic programming solution where f[i][j] represents the maximum number of queries that can be processed when elements in the interval [i, j] haven't been deleted yet.

Initialization:

  • Create a 2D array f of size n × n initialized with zeros
  • n is the length of nums and m is the length of queries

State Transitions: We iterate through all possible intervals [i, j] and compute f[i][j] based on two possible previous states:

  1. Transition from f[i-1][j] (when i > 0):

    • This means we just removed nums[i-1] to reach the current state
    • We can process one more query if nums[i-1] >= queries[f[i-1][j]]
    • Update: f[i][j] = max(f[i][j], f[i-1][j] + (nums[i-1] >= queries[f[i-1][j]]))
  2. Transition from f[i][j+1] (when j + 1 < n):

    • This means we just removed nums[j+1] to reach the current state
    • We can process one more query if nums[j+1] >= queries[f[i][j+1]]
    • Update: f[i][j] = max(f[i][j], f[i][j+1] + (nums[j+1] >= queries[f[i][j+1]]))

Early Termination:

  • If f[i][j] == m at any point, we've processed all queries, so return m immediately

Computing the Final Answer: The answer is the maximum over all possible last remaining elements:

  • For each i from 0 to n-1, consider f[i][i] (queries processed before reaching single element i)
  • Add 1 if the last element nums[i] can handle the next query: nums[i] >= queries[f[i][i]]
  • Return: max(f[i][i] + (nums[i] >= queries[f[i][i]]) for i in range(n))

Implementation Details:

  • The outer loop iterates i from 0 to n-1
  • The inner loop iterates j from n-1 down to i (ensuring we process larger intervals before smaller ones)
  • The boolean expression (nums[x] >= queries[y]) evaluates to 1 if true, 0 if false, making the transition formula concise

This approach efficiently explores all possible subsequences and removal strategies in O(n²) time and space.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with nums = [3, 1, 4] and queries = [2, 3].

We'll build our DP table f[i][j] where f[i][j] represents the maximum queries processed when interval [i, j] remains.

Step 1: Initialize

  • Create a 3×3 table (since n=3) filled with zeros
  • Our goal is to fill this table and find the maximum answer

Step 2: Fill the DP table

Let's process intervals from largest to smallest:

Interval [0, 2] (entire array):

  • No previous states exist (this is the full array)
  • f[0][2] = 0 (no queries processed yet)

Interval [1, 2]:

  • Can come from f[0][2] by removing nums[0]=3
  • Check: Can we remove 3? We need 3 >= queries[0]=2
  • f[1][2] = f[0][2] + 1 = 0 + 1 = 1

Interval [0, 1]:

  • Can come from f[0][2] by removing nums[2]=4
  • Check: Can we remove 4? We need 4 >= queries[0]=2
  • f[0][1] = f[0][2] + 1 = 0 + 1 = 1

Interval [0, 0] (just element 3):

  • Can come from f[0][1] by removing nums[1]=1
  • Check: Can we remove 1? We need 1 >= queries[1]=3
  • f[0][0] = f[0][1] + 0 = 1

Interval [1, 1] (just element 1):

  • Can come from f[0][1] by removing nums[0]=3
  • Check: Can we remove 3? We need 3 >= queries[1]=3
  • f[1][1] = f[0][1] + 1 = 1 + 1 = 2

Interval [2, 2] (just element 4):

  • Can come from f[1][2] by removing nums[1]=1
  • Check: Can we remove 1? We need 1 >= queries[1]=3
  • f[2][2] = f[1][2] + 0 = 1

Step 3: Compute final answer

For each single element, check if it can handle one more query:

  • f[0][0] = 1, can nums[0]=3 handle queries[1]=3? Yes! Total: 1 + 1 = 2
  • f[1][1] = 2, can nums[1]=1 handle queries[2]? (out of bounds, so 0). Total: 2 + 0 = 2
  • f[2][2] = 1, can nums[2]=4 handle queries[1]=3? Yes! Total: 1 + 1 = 2

Maximum = 2

Verification: This corresponds to choosing subsequence [3, 1] initially:

  • Process queries[0]=2: Remove 1 (since 1 ≥ 2 is false, we can't remove it... wait)

Actually, let me reconsider. The optimal path is:

  • Start with full array [3, 1, 4]
  • Process queries[0]=2: Remove 4 (4 ≥ 2), leaving [3, 1]
  • Process queries[1]=3: Remove 3 (3 ≥ 3), leaving [1]
  • Cannot process more queries since 1 < 3

So we successfully process 2 queries, matching our DP result!

Solution Implementation

1class Solution:
2    def maximumProcessableQueries(self, nums: List[int], queries: List[int]) -> int:
3        n = len(nums)
4        m = len(queries)
5      
6        # dp[left][right] represents the maximum number of queries that can be processed
7        # when we've taken 'left' elements from the left side and 
8        # elements from index 'right+1' to 'n-1' from the right side
9        dp = [[0] * n for _ in range(n)]
10      
11        # Iterate through all possible states
12        for left in range(n):
13            for right in range(n - 1, left - 1, -1):
14                # Try taking an element from the left side
15                if left > 0:
16                    prev_queries_processed = dp[left - 1][right]
17                    # Check if the previous element from left can satisfy the next query
18                    can_process_next = 1 if nums[left - 1] >= queries[prev_queries_processed] else 0
19                    dp[left][right] = max(
20                        dp[left][right], 
21                        prev_queries_processed + can_process_next
22                    )
23              
24                # Try taking an element from the right side
25                if right + 1 < n:
26                    prev_queries_processed = dp[left][right + 1]
27                    # Check if the next element from right can satisfy the next query
28                    can_process_next = 1 if nums[right + 1] >= queries[prev_queries_processed] else 0
29                    dp[left][right] = max(
30                        dp[left][right], 
31                        prev_queries_processed + can_process_next
32                    )
33              
34                # Early termination: if we can process all queries, return immediately
35                if dp[left][right] == m:
36                    return m
37      
38        # Check the maximum queries processable when taking only one element
39        # For each position i, check if nums[i] can satisfy the next query
40        max_queries = 0
41        for i in range(n):
42            queries_processed = dp[i][i]
43            # Check if the element at position i can satisfy the next query
44            can_process_next = 1 if nums[i] >= queries[queries_processed] else 0
45            max_queries = max(max_queries, queries_processed + can_process_next)
46      
47        return max_queries
48
1class Solution {
2    public int maximumProcessableQueries(int[] nums, int[] queries) {
3        int numsLength = nums.length;
4        int queriesLength = queries.length;
5      
6        // dp[left][right] represents the maximum number of queries that can be processed
7        // when we've already removed elements from index 0 to left-1 (from the left side)
8        // and from index right+1 to numsLength-1 (from the right side)
9        int[][] dp = new int[numsLength][numsLength];
10      
11        // Fill the DP table by considering all possible ranges [left, right]
12        for (int left = 0; left < numsLength; ++left) {
13            for (int right = numsLength - 1; right >= left; --right) {
14              
15                // Case 1: Try extending from the state where we previously removed nums[left-1]
16                // Check if we can process one more query by removing nums[left-1]
17                if (left > 0) {
18                    int previousQueries = dp[left - 1][right];
19                    int additionalQuery = (nums[left - 1] >= queries[previousQueries] ? 1 : 0);
20                    dp[left][right] = Math.max(dp[left][right], previousQueries + additionalQuery);
21                }
22              
23                // Case 2: Try extending from the state where we previously removed nums[right+1]
24                // Check if we can process one more query by removing nums[right+1]
25                if (right + 1 < numsLength) {
26                    int previousQueries = dp[left][right + 1];
27                    int additionalQuery = (nums[right + 1] >= queries[previousQueries] ? 1 : 0);
28                    dp[left][right] = Math.max(dp[left][right], previousQueries + additionalQuery);
29                }
30              
31                // Early termination: if we can already process all queries, return immediately
32                if (dp[left][right] == queriesLength) {
33                    return queriesLength;
34                }
35            }
36        }
37      
38        // Calculate the final answer by checking each possible last remaining element
39        int maxProcessableQueries = 0;
40        for (int i = 0; i < numsLength; ++i) {
41            // For each element nums[i] as the last remaining element,
42            // check if we can process one more query with it
43            int queriesProcessed = dp[i][i];
44            int additionalQuery = (nums[i] >= queries[queriesProcessed] ? 1 : 0);
45            maxProcessableQueries = Math.max(maxProcessableQueries, queriesProcessed + additionalQuery);
46        }
47      
48        return maxProcessableQueries;
49    }
50}
51
1class Solution {
2public:
3    int maximumProcessableQueries(vector<int>& nums, vector<int>& queries) {
4        int n = nums.size();
5        int m = queries.size();
6      
7        // dp[left][right] represents the maximum number of queries that can be processed
8        // when we have already used elements from nums[0...left-1] and nums[right+1...n-1]
9        // In other words, available elements are nums[left...right]
10        int dp[n][n];
11        memset(dp, 0, sizeof(dp));
12      
13        // Fill the DP table
14        // We iterate through all possible intervals [left, right]
15        for (int left = 0; left < n; ++left) {
16            for (int right = n - 1; right >= left; --right) {
17                // Try extending from the previous state by taking the left element
18                // If we previously had interval [left-1, right], we can take nums[left-1]
19                if (left > 0) {
20                    int prevQueries = dp[left - 1][right];
21                    // Check if nums[left-1] can satisfy the next query
22                    int canProcess = (nums[left - 1] >= queries[prevQueries]) ? 1 : 0;
23                    dp[left][right] = max(dp[left][right], prevQueries + canProcess);
24                }
25              
26                // Try extending from the previous state by taking the right element
27                // If we previously had interval [left, right+1], we can take nums[right+1]
28                if (right + 1 < n) {
29                    int prevQueries = dp[left][right + 1];
30                    // Check if nums[right+1] can satisfy the next query
31                    int canProcess = (nums[right + 1] >= queries[prevQueries]) ? 1 : 0;
32                    dp[left][right] = max(dp[left][right], prevQueries + canProcess);
33                }
34              
35                // Early termination: if we can process all queries, return immediately
36                if (dp[left][right] == m) {
37                    return m;
38                }
39            }
40        }
41      
42        // Find the maximum queries we can process by choosing the last remaining element
43        int maxQueries = 0;
44        for (int i = 0; i < n; ++i) {
45            // For each possible last element nums[i], check if it can process one more query
46            int processedSoFar = dp[i][i];
47            int canProcessLast = (nums[i] >= queries[processedSoFar]) ? 1 : 0;
48            maxQueries = max(maxQueries, processedSoFar + canProcessLast);
49        }
50      
51        return maxQueries;
52    }
53};
54
1/**
2 * Finds the maximum number of queries that can be processed
3 * @param nums - Array of numbers to process queries against
4 * @param queries - Array of query values to be processed
5 * @returns Maximum number of processable queries
6 */
7function maximumProcessableQueries(nums: number[], queries: number[]): number {
8    const numsLength: number = nums.length;
9    const queriesLength: number = queries.length;
10  
11    // Initialize 2D DP array where dp[i][j] represents the maximum number of queries
12    // that can be processed when considering the range from index i to j
13    const dp: number[][] = Array.from(
14        { length: numsLength }, 
15        () => Array.from({ length: numsLength }, () => 0)
16    );
17  
18    // Fill the DP table using dynamic programming
19    for (let leftIndex = 0; leftIndex < numsLength; ++leftIndex) {
20        for (let rightIndex = numsLength - 1; rightIndex >= leftIndex; --rightIndex) {
21            // Try extending from the left side
22            if (leftIndex > 0) {
23                const previousLeft: number = dp[leftIndex - 1][rightIndex];
24                const canProcessQuery: number = nums[leftIndex - 1] >= queries[previousLeft] ? 1 : 0;
25                dp[leftIndex][rightIndex] = Math.max(
26                    dp[leftIndex][rightIndex],
27                    previousLeft + canProcessQuery
28                );
29            }
30          
31            // Try extending from the right side
32            if (rightIndex + 1 < numsLength) {
33                const previousRight: number = dp[leftIndex][rightIndex + 1];
34                const canProcessQuery: number = nums[rightIndex + 1] >= queries[previousRight] ? 1 : 0;
35                dp[leftIndex][rightIndex] = Math.max(
36                    dp[leftIndex][rightIndex],
37                    previousRight + canProcessQuery
38                );
39            }
40          
41            // Early termination if all queries can be processed
42            if (dp[leftIndex][rightIndex] === queriesLength) {
43                return queriesLength;
44            }
45        }
46    }
47  
48    // Find the maximum result by checking each single element position
49    let maxProcessableQueries: number = 0;
50    for (let index = 0; index < numsLength; ++index) {
51        const currentProcessed: number = dp[index][index];
52        const canProcessNext: number = nums[index] >= queries[currentProcessed] ? 1 : 0;
53        maxProcessableQueries = Math.max(
54            maxProcessableQueries, 
55            currentProcessed + canProcessNext
56        );
57    }
58  
59    return maxProcessableQueries;
60}
61

Time and Space Complexity

The time complexity is O(n²), where n is the length of the array nums.

The algorithm uses a nested loop structure where:

  • The outer loop iterates through i from 0 to n-1, giving O(n) iterations
  • The inner loop iterates through j from n-1 down to i, which in the worst case gives O(n) iterations
  • Inside the nested loops, all operations (comparisons, array accesses, max operations) are O(1)
  • The final return statement iterates through n elements once, adding O(n) time

Therefore, the overall time complexity is O(n) × O(n) + O(n) = O(n²).

The space complexity is O(n²), where n is the length of the array nums.

The algorithm creates a 2D array f with dimensions n × n, which requires O(n²) space to store all the elements. The remaining variables (i, j, m) use constant space O(1).

Therefore, the total space complexity is O(n²).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Index Out of Bounds When Checking queries[f[i][j]]

The Pitfall: When computing transitions, the code checks queries[f[i][j]] or queries[f[i-1][j]]. If f[i][j] equals m (the length of queries), this causes an index out of bounds error since valid indices are 0 to m-1.

Why It Happens:

  • As we process queries successfully, f[i][j] increments
  • When all queries are processed, f[i][j] = m
  • Attempting to access queries[m] throws an IndexError

Solution: Add boundary checks before accessing the queries array:

# Instead of:
can_process_next = 1 if nums[left - 1] >= queries[prev_queries_processed] else 0

# Use:
can_process_next = 0
if prev_queries_processed < m:
    can_process_next = 1 if nums[left - 1] >= queries[prev_queries_processed] else 0

2. Incorrect Understanding of State Representation

The Pitfall: Misinterpreting what dp[i][j] represents. The state actually represents elements taken from both ends, not a contiguous interval [i, j].

Clarification:

  • dp[left][right] means we've removed left elements from the start and (n-1-right) elements from the end
  • The remaining elements are from index left to right (inclusive)
  • This is NOT about selecting a subsequence [i, j] from the original array

Solution: Ensure the mental model is correct: we're simulating removing elements from both ends of the array, not selecting a contiguous subarray.

3. Forgetting the Final Single Element Check

The Pitfall: The main DP computation handles transitions between states, but doesn't account for processing the very last element. The code might return dp[i][i] directly without checking if the last remaining element can handle one more query.

Why It Matters: When only one element remains (dp[i][i]), we still need to check if this element can process the next query in the sequence.

Solution: Always add the final check as shown in the code:

for i in range(n):
    queries_processed = dp[i][i]
    if queries_processed < m:  # Add this safety check
        can_process_next = 1 if nums[i] >= queries[queries_processed] else 0
        max_queries = max(max_queries, queries_processed + can_process_next)
    else:
        max_queries = m  # All queries already processed
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

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

Load More