Facebook Pixel

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.

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

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 with x as its last element
  • If x can extend some subsequences, we choose the one with the largest valid last element and replace it with x

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)
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 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}
43
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 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};
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 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}
47

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:

  1. Initialize an empty array g: This array will store the last element of each subsequence we're building. The array g will maintain a decreasing order throughout the process.

  2. Process each element x in nums:

    • We need to find the position where x should be placed in g
    • Since g is in decreasing order, we want to find the first element in g that is smaller than x
    • This is where binary search comes in
  3. 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] < x identifies positions where we can append x
    • We search for the first (leftmost) such position by moving right = mid - 1 when feasible
    • first_true_index tracks the answer; if no valid position exists, it remains -1
  4. 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 in g is smaller than x, so we start a new subsequence
    • Otherwise, we replace g[first_true_index] with x, extending that subsequence
  5. 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 is false, no element < 3, append → g = [5, 3]
  • Process 4: Find first element < 4 (which is 3 at index 1), replace → g = [5, 4]
  • Process 1: No element < 1, append → g = [5, 4, 1]
  • Process 2: Find first element < 2 (which is 1 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 Evaluator

Example 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 4
  • g = [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, so l = 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, so l = 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.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More