3231. Minimum Number of Increasing Subsequence to Be Removed
Problem Description
You are given an array of integers nums. You can perform the following operation any number of times:
- Remove a strictly increasing subsequence from the array
Your goal is to find the minimum number of operations needed to make the array empty.
A strictly increasing subsequence means selecting elements from the array (maintaining their relative order) where each element is strictly greater than the previous one. For example, if nums = [5, 3, 4, 1, 2], you could remove [3, 4] as one operation since 3 < 4, or remove [1, 2] since 1 < 2.
The challenge is to strategically choose which subsequences to remove so that the total number of operations is minimized. Each operation removes one complete strictly increasing subsequence, and you need to determine the fewest number of such operations to remove all elements from the array.
For instance, if nums = [5, 3, 4, 1, 2], one way would be:
- Operation 1: Remove
[3, 4]→ remaining array:[5, 1, 2] - Operation 2: Remove
[1, 2]→ remaining array:[5] - Operation 3: Remove
[5]→ array becomes empty
This takes 3 operations total, which happens to be the minimum for this example.
Intuition
Let's think about this problem from a different angle. Instead of focusing on which subsequences to remove, let's consider what happens when we can't add an element to any existing subsequence.
When we traverse the array from left to right, for each element x, we want to append it to one of the subsequences we're currently building. To maintain the strictly increasing property, we can only append x to a subsequence whose last element is less than x.
Here's the key insight: if we greedily choose to append x to the subsequence with the largest last element that is still less than x, we maximize our chances of being able to add future elements to this subsequence. This is because we're keeping the last element as small as possible while still being valid.
Now, what happens to the last elements of all our subsequences as we build them? If we always make the greedy choice above, the last elements of our subsequences will be in decreasing order. Why? Because when a new element x comes in:
- If
xis larger than all existing last elements, we start a new subsequence withxas its last element - If
xcan extend some subsequences, we choose the one with the largest valid last element and replace it withx
This decreasing order property is crucial because it allows us to use binary search to quickly find which subsequence to extend.
The minimum number of operations equals the number of subsequences we need to build. Each subsequence corresponds to one removal operation. By greedily minimizing the number of subsequences (through smart placement of each element), we minimize the total number of operations needed.
Think of it like stacking items where each stack must be strictly increasing from bottom to top. We want to minimize the number of stacks. The greedy strategy ensures we use the minimum number of stacks possible.
Learn more about Binary Search patterns.
Solution Implementation
1class Solution:
2 def minOperations(self, nums: List[int]) -> int:
3 # Array to maintain elements in non-increasing order
4 # Each element represents the smallest ending value of a non-increasing subsequence of that length
5 non_increasing_subsequences = []
6
7 for current_num in nums:
8 # Binary search to find the first position where element < current_num
9 # Using the standard template with first_true_index
10 left, right = 0, len(non_increasing_subsequences) - 1
11 first_true_index = -1
12
13 while left <= right:
14 mid = (left + right) // 2
15
16 if non_increasing_subsequences[mid] < current_num:
17 # Found an element smaller than current_num
18 # Record this position and search left for earlier occurrence
19 first_true_index = mid
20 right = mid - 1
21 else:
22 # Element at mid is >= current_num, search right half
23 left = mid + 1
24
25 # After binary search, first_true_index is the position to replace
26 if first_true_index == -1:
27 # No element is smaller than current_num
28 # Append it to start a new subsequence
29 non_increasing_subsequences.append(current_num)
30 else:
31 # Replace the element at first_true_index with current_num
32 # This extends the subsequence at this position
33 non_increasing_subsequences[first_true_index] = current_num
34
35 # The length of the array represents the minimum number of operations
36 return len(non_increasing_subsequences)
371class Solution {
2 public int minOperations(int[] nums) {
3 // List to maintain the smallest ending values of decreasing subsequences
4 // Each element represents the minimum last element of a decreasing subsequence
5 List<Integer> decreasingSubsequences = new ArrayList<>();
6
7 for (int currentNum : nums) {
8 // Binary search to find the first position where element < currentNum
9 // Using the standard template with firstTrueIndex
10 int left = 0;
11 int right = decreasingSubsequences.size() - 1;
12 int firstTrueIndex = -1;
13
14 while (left <= right) {
15 int mid = left + (right - left) / 2;
16
17 if (decreasingSubsequences.get(mid) < currentNum) {
18 // Found an element smaller than currentNum
19 // Record this position and search left for earlier occurrence
20 firstTrueIndex = mid;
21 right = mid - 1;
22 } else {
23 // Element at mid is >= currentNum, search right half
24 left = mid + 1;
25 }
26 }
27
28 if (firstTrueIndex == -1) {
29 // No element is smaller than currentNum
30 // Start a new decreasing subsequence
31 decreasingSubsequences.add(currentNum);
32 } else {
33 // Replace the element at firstTrueIndex with currentNum
34 // This extends the subsequence at this position
35 decreasingSubsequences.set(firstTrueIndex, currentNum);
36 }
37 }
38
39 // Return the number of decreasing subsequences needed
40 return decreasingSubsequences.size();
41 }
42}
431class Solution {
2public:
3 int minOperations(vector<int>& nums) {
4 // Store the last (smallest) element of each decreasing subsequence
5 // Each element in this vector represents a separate decreasing subsequence
6 vector<int> decreasingSubsequences;
7
8 for (int currentNum : nums) {
9 // Binary search to find the first position where element < currentNum
10 // Using the standard template with firstTrueIndex
11 int left = 0;
12 int right = decreasingSubsequences.size() - 1;
13 int firstTrueIndex = -1;
14
15 while (left <= right) {
16 int mid = left + (right - left) / 2;
17
18 if (decreasingSubsequences[mid] < currentNum) {
19 // Found an element smaller than currentNum
20 // Record this position and search left for earlier occurrence
21 firstTrueIndex = mid;
22 right = mid - 1;
23 } else {
24 // Element at mid is >= currentNum, search right half
25 left = mid + 1;
26 }
27 }
28
29 // After binary search, firstTrueIndex is the position to replace
30 if (firstTrueIndex == -1) {
31 // No element is smaller than currentNum, create a new subsequence
32 decreasingSubsequences.push_back(currentNum);
33 } else {
34 // Replace the element at firstTrueIndex with currentNum
35 decreasingSubsequences[firstTrueIndex] = currentNum;
36 }
37 }
38
39 // Return the number of decreasing subsequences needed
40 return decreasingSubsequences.size();
41 }
42};
431/**
2 * Finds the minimum number of operations to make the array non-increasing
3 * by finding the longest non-increasing subsequence
4 * @param nums - The input array of numbers
5 * @returns The minimum number of operations needed
6 */
7function minOperations(nums: number[]): number {
8 // Array to store the longest non-increasing subsequence
9 const longestNonIncreasingSubsequence: number[] = [];
10
11 // Iterate through each number in the input array
12 for (const currentNumber of nums) {
13 // Binary search to find the first position where element < currentNumber
14 // Using the standard template with firstTrueIndex
15 let left: number = 0;
16 let right: number = longestNonIncreasingSubsequence.length - 1;
17 let firstTrueIndex: number = -1;
18
19 while (left <= right) {
20 const mid: number = Math.floor((left + right) / 2);
21
22 if (longestNonIncreasingSubsequence[mid] < currentNumber) {
23 // Found an element smaller than currentNumber
24 // Record this position and search left for earlier occurrence
25 firstTrueIndex = mid;
26 right = mid - 1;
27 } else {
28 // Element at mid is >= currentNumber, search right half
29 left = mid + 1;
30 }
31 }
32
33 // After binary search, firstTrueIndex is the position to replace
34 if (firstTrueIndex === -1) {
35 // No element is smaller than currentNumber, append it
36 longestNonIncreasingSubsequence.push(currentNumber);
37 } else {
38 // Replace the element at firstTrueIndex with currentNumber
39 longestNonIncreasingSubsequence[firstTrueIndex] = currentNumber;
40 }
41 }
42
43 // The length of the longest non-increasing subsequence
44 // represents the minimum number of operations
45 return longestNonIncreasingSubsequence.length;
46}
47Solution Approach
Based on our intuition, we need to maintain the last elements of our subsequences in decreasing order. We'll use an array g to store these last elements.
Here's how the algorithm works:
-
Initialize an empty array
g: This array will store the last element of each subsequence we're building. The arraygwill maintain a decreasing order throughout the process. -
Process each element
xinnums:- We need to find the position where
xshould be placed ing - Since
gis in decreasing order, we want to find the first element ingthat is smaller thanx - This is where binary search comes in
- We need to find the position where
-
Binary Search Implementation: We use the standard boundary-finding template to find the first position where
g[mid] < x:left, right = 0, len(g) - 1 first_true_index = -1 while left <= right: mid = (left + right) // 2 if g[mid] < x: # feasible condition: element is smaller than x first_true_index = mid right = mid - 1 # search left for earlier position else: left = mid + 1- The feasible condition
g[mid] < xidentifies positions where we can appendx - We search for the first (leftmost) such position by moving
right = mid - 1when feasible first_true_indextracks the answer; if no valid position exists, it remains-1
- The feasible condition
-
Update the array
g:if first_true_index == -1: g.append(x) # x is smaller than or equal to all elements else: g[first_true_index] = x # extend the subsequence at this position- If
first_true_index == -1, no element ingis smaller thanx, so we start a new subsequence - Otherwise, we replace
g[first_true_index]withx, extending that subsequence
- If
-
Return the result: The length of
grepresents the minimum number of subsequences needed, which equals the minimum number of operations.
Example walkthrough with nums = [5, 3, 4, 1, 2]:
- Process
5:g = [5] - Process
3:3 < 5is false, no element < 3, append →g = [5, 3] - Process
4: Find first element< 4(which is3at index 1), replace →g = [5, 4] - Process
1: No element < 1, append →g = [5, 4, 1] - Process
2: Find first element< 2(which is1at index 2), replace →g = [5, 4, 2] - Result:
len(g) = 3
The time complexity is O(n log n) where n is the length of the input array, due to the binary search for each element. The space complexity is O(n) in the worst case when all elements form a decreasing sequence.
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 the solution with nums = [4, 6, 3, 5, 2]:
Initial state: g = [] (empty array to track last elements of subsequences)
Step 1: Process element 4
gis empty, so append 4g = [4]- This represents starting our first subsequence ending with 4
Step 2: Process element 6
- Binary search in
g = [4]for first element < 6 - Found 4 at index 0, so replace
g[0]with 6 g = [6]- We extended the subsequence [4] to [4, 6]
Step 3: Process element 3
- Binary search in
g = [6]for first element < 3 - No element in
gis < 3, sol = len(g) = 1 - Append 3 to
g g = [6, 3]- Started a second subsequence with just [3]
Step 4: Process element 5
- Binary search in
g = [6, 3]for first element < 5 - Found 3 at index 1, so replace
g[1]with 5 g = [6, 5]- Extended the second subsequence from [3] to [3, 5]
Step 5: Process element 2
- Binary search in
g = [6, 5]for first element < 2 - No element in
gis < 2, sol = len(g) = 2 - Append 2 to
g g = [6, 5, 2]- Started a third subsequence with just [2]
Final result: len(g) = 3, meaning we need 3 operations
The three subsequences we built were:
- Subsequence 1: [4, 6]
- Subsequence 2: [3, 5]
- Subsequence 3: [2]
Each subsequence represents one removal operation, giving us the minimum of 3 operations needed to empty the array.
Time and Space Complexity
The time complexity of this algorithm is O(n log n), where n is the length of the array nums.
The algorithm iterates through each element in nums exactly once, which takes O(n) time. For each element, it performs a binary search on the array g to find the appropriate position. The binary search operation takes O(log k) time, where k is the current size of array g. In the worst case, g can grow up to size n, so the binary search can take up to O(log n) time. Therefore, the overall time complexity is O(n) × O(log n) = O(n log n).
The space complexity is O(n), where n is the length of the array nums.
The algorithm uses an auxiliary array g to store elements. In the worst case, when all elements in nums form a strictly decreasing sequence, the array g will store all n elements from the input array. Therefore, the space complexity is O(n).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using Wrong Binary Search Template Variant
The most common pitfall is using a non-standard binary search template that doesn't properly track the answer. Many developers mistakenly return left directly without tracking first_true_index:
Incorrect Implementation:
while left < right: mid = (left + right) >> 1 if non_increasing_subsequences[mid] < current_num: right = mid else: left = mid + 1 return left # WRONG! Doesn't handle edge cases properly
Correct Implementation (Template):
left, right = 0, len(non_increasing_subsequences) - 1
first_true_index = -1
while left <= right:
mid = (left + right) // 2
if non_increasing_subsequences[mid] < current_num:
first_true_index = mid
right = mid - 1
else:
left = mid + 1
# Use first_true_index, which is -1 if no valid position found
2. Confusion About Binary Search Direction
Another pitfall is misunderstanding which comparison operator to use. We need to find the first element that is less than current_num, so we can extend that subsequence:
Why g[mid] < current_num: We're maintaining subsequences where we can append strictly increasing elements. Finding an element less than current_num means current_num can be appended after it.
3. Misunderstanding the Array's Purpose
The array non_increasing_subsequences doesn't store actual subsequences - it stores the last element of each subsequence. The array maintains these last elements in non-increasing order.
For example, with nums = [5, 3, 3, 2]:
- After processing:
g = [5, 3, 3, 2] - This represents 4 different subsequences, each containing one element
- The array can have duplicates (non-increasing, not strictly decreasing)
4. Off-by-One Error in Binary Search Bounds
When using the template, the right bound should be len(array) - 1 (not len(array)), because we use while left <= right:
Correct:
left, right = 0, len(non_increasing_subsequences) - 1
first_true_index = -1
while left <= right: # Use <= with right = len - 1
# ...
When first_true_index remains -1, it means no element is smaller than current_num, so we need to append to create a new subsequence.
You are given an array of intervals where intervals[i] = [start_i, end_i] represent the start and end of the ith interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!