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
x
is larger than all existing last elements, we start a new subsequence withx
as its last element - If
x
can 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 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 arrayg
will maintain a decreasing order throughout the process. -
Process each element
x
innums
:- We need to find the position where
x
should be placed ing
- Since
g
is in decreasing order, we want to find the first element ing
that is smaller thanx
- This is where binary search comes in
- We need to find the position where
-
Binary Search Implementation:
l, r = 0, len(g) while l < r: mid = (l + r) >> 1 if g[mid] < x: r = mid else: l = mid + 1
- We search for the leftmost position where
g[mid] < x
- The condition
g[mid] < x
means we can appendx
to the subsequence ending withg[mid]
- After the binary search,
l
points to the position where we should placex
- We search for the leftmost position where
-
Update the array
g
:if l == len(g): g.append(x) else: g[l] = x
- If
l == len(g)
, it meansx
is smaller than all elements ing
, so we need to start a new subsequence (appendx
tog
) - Otherwise, we replace
g[l]
withx
, which means we're extending the subsequence that previously ended withg[l]
- If
-
Return the result: The length of
g
represents 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 < 5
, append →g = [5, 3]
- Process
4
: Find first element< 4
(which is3
at index 1), replace →g = [5, 4]
- Process
1
:1
is smaller than all, append →g = [5, 4, 1]
- Process
2
: Find first element< 2
(which is1
at 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
g
is 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
g
is < 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
g
is < 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.
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 rightmost position where current_num can be placed
9 # We're looking for the rightmost element that is less than current_num
10 left, right = 0, len(non_increasing_subsequences)
11
12 while left < right:
13 mid = (left + right) >> 1 # Equivalent to (left + right) // 2
14
15 if non_increasing_subsequences[mid] < current_num:
16 # Current element at mid is less than current_num
17 # We need to search in the left half (including mid)
18 right = mid
19 else:
20 # Current element at mid is >= current_num
21 # We need to search in the right half (excluding mid)
22 left = mid + 1
23
24 # After binary search, left is the position to insert/replace
25 if left == len(non_increasing_subsequences):
26 # current_num is smaller than or equal to all existing elements
27 # Append it to extend the subsequence
28 non_increasing_subsequences.append(current_num)
29 else:
30 # Replace the element at position left with current_num
31 # This maintains the non-increasing property while potentially allowing longer subsequences
32 non_increasing_subsequences[left] = current_num
33
34 # The length of the array represents the minimum number of operations
35 # (or the length of the longest non-increasing subsequence)
36 return len(non_increasing_subsequences)
37
1class 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 rightmost subsequence where we can append currentNum
9 // We're looking for a subsequence ending with a value >= currentNum
10 int left = 0;
11 int right = decreasingSubsequences.size();
12
13 while (left < right) {
14 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
15
16 if (decreasingSubsequences.get(mid) < currentNum) {
17 // Current subsequence cannot accommodate this number
18 // Look in the left half for larger ending values
19 right = mid;
20 } else {
21 // Current subsequence can accommodate this number
22 // Try to find a better position in the right half
23 left = mid + 1;
24 }
25 }
26
27 if (left == decreasingSubsequences.size()) {
28 // No existing subsequence can accommodate currentNum
29 // Start a new decreasing subsequence
30 decreasingSubsequences.add(currentNum);
31 } else {
32 // Update the ending value of an existing subsequence
33 // Replace with currentNum as the new minimum ending value
34 decreasingSubsequences.set(left, currentNum);
35 }
36 }
37
38 // Return the number of decreasing subsequences needed
39 return decreasingSubsequences.size();
40 }
41}
42
1class 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 rightmost subsequence where we can append currentNum
10 // We're looking for a subsequence whose last element is greater than currentNum
11 // (so currentNum can extend that decreasing subsequence)
12 int left = 0;
13 int right = decreasingSubsequences.size();
14
15 while (left < right) {
16 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
17
18 if (decreasingSubsequences[mid] < currentNum) {
19 // Current subsequence's last element is smaller than currentNum
20 // Can't extend this subsequence, search in left half
21 right = mid;
22 } else {
23 // Current subsequence's last element is >= currentNum
24 // Can potentially extend this, but check if there's a better option to the right
25 left = mid + 1;
26 }
27 }
28
29 // After binary search, left points to the position where we should place currentNum
30 if (left == decreasingSubsequences.size()) {
31 // No suitable subsequence found, create a new one
32 decreasingSubsequences.push_back(currentNum);
33 } else {
34 // Found a subsequence to extend, update its last element
35 decreasingSubsequences[left] = currentNum;
36 }
37 }
38
39 // Return the number of decreasing subsequences needed
40 return decreasingSubsequences.size();
41 }
42};
43
1/**
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 position to insert/replace the current number
14 let left: number = 0;
15 let right: number = longestNonIncreasingSubsequence.length;
16
17 while (left < right) {
18 // Calculate middle index using bit shift for efficiency
19 const mid: number = (left + right) >> 1;
20
21 // Since we're building a non-increasing sequence,
22 // we look for the first element smaller than current number
23 if (longestNonIncreasingSubsequence[mid] < currentNumber) {
24 right = mid;
25 } else {
26 left = mid + 1;
27 }
28 }
29
30 // If left reaches the end, append the current number
31 if (left === longestNonIncreasingSubsequence.length) {
32 longestNonIncreasingSubsequence.push(currentNumber);
33 } else {
34 // Otherwise, replace the element at position left
35 longestNonIncreasingSubsequence[left] = currentNumber;
36 }
37 }
38
39 // The length of the longest non-increasing subsequence
40 // represents the minimum number of operations
41 return longestNonIncreasingSubsequence.length;
42}
43
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. Confusion About Binary Search Direction
The most common pitfall is misunderstanding which comparison operator to use in the binary search. Many developers mistakenly write:
Incorrect Implementation:
if non_increasing_subsequences[mid] > current_num: # WRONG! right = mid else: left = mid + 1
Why this is wrong: We're maintaining subsequences where we can append strictly increasing elements. We need to find where current_num
can extend an existing subsequence, which means finding an element that is less than current_num
(so current_num
can be appended after it).
Correct Implementation:
if non_increasing_subsequences[mid] < current_num: # CORRECT right = mid else: left = mid + 1
2. Misunderstanding the Array's Purpose
Another pitfall is thinking the array non_increasing_subsequences
stores actual subsequences or that it must be strictly decreasing.
What the array actually represents:
- Each position
i
in the array represents a subsequence - The value at position
i
is the last element of that subsequence - The array maintains these last elements in non-increasing order (not strictly decreasing)
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)
3. Off-by-One Error in Binary Search Bounds
Incorrect:
left, right = 0, len(non_increasing_subsequences) - 1 # WRONG!
Correct:
left, right = 0, len(non_increasing_subsequences) # CORRECT
The right bound should be len(array)
(not len(array) - 1
) because we might need to append to the end of the array when no suitable position is found.
4. Using Wrong Binary Search Template
Some might use a different binary search template that doesn't work correctly for this problem:
Incorrect Template:
while left <= right: # WRONG for this problem mid = (left + right) // 2 if non_increasing_subsequences[mid] < current_num: right = mid - 1 else: left = mid + 1
Correct Template:
while left < right: # CORRECT mid = (left + right) >> 1 if non_increasing_subsequences[mid] < current_num: right = mid else: left = mid + 1
The correct template ensures we find the leftmost position where we can place current_num
, maintaining the non-increasing property of the array.
How does quick sort divide the problem into subproblems?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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!