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 thanqueries[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 toqueries[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.
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 statef[i-1][j]
- We might have just removed element at index
j+1
(if it exists), meaning we came from statef[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 sizen × n
initialized with zeros n
is the length ofnums
andm
is the length ofqueries
State Transitions:
We iterate through all possible intervals [i, j]
and compute f[i][j]
based on two possible previous states:
-
Transition from
f[i-1][j]
(wheni > 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]]))
- This means we just removed
-
Transition from
f[i][j+1]
(whenj + 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]]))
- This means we just removed
Early Termination:
- If
f[i][j] == m
at any point, we've processed all queries, so returnm
immediately
Computing the Final Answer: The answer is the maximum over all possible last remaining elements:
- For each
i
from 0 ton-1
, considerf[i][i]
(queries processed before reaching single elementi
) - 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 ton-1
- The inner loop iterates
j
fromn-1
down toi
(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 EvaluatorExample 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 removingnums[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 removingnums[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 removingnums[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 removingnums[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 removingnums[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
, cannums[0]=3
handlequeries[1]=3
? Yes! Total: 1 + 1 = 2f[1][1] = 2
, cannums[1]=1
handlequeries[2]
? (out of bounds, so 0). Total: 2 + 0 = 2f[2][2] = 1
, cannums[2]=4
handlequeries[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
from0
ton-1
, givingO(n)
iterations - The inner loop iterates through
j
fromn-1
down toi
, which in the worst case givesO(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, addingO(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 removedleft
elements from the start and(n-1-right)
elements from the end- The remaining elements are from index
left
toright
(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
How many ways can you arrange the three letters A, B and C?
Recommended Readings
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!