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 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:

    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 append x to the subsequence ending with g[mid]
    • After the binary search, l points to the position where we should place x
  4. Update the array g:

    if l == len(g):
        g.append(x)
    else:
        g[l] = x
    • If l == len(g), it means x is smaller than all elements in g, so we need to start a new subsequence (append x to g)
    • Otherwise, we replace g[l] with x, which means we're extending the subsequence that previously ended with g[l]
  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, append → g = [5, 3]
  • Process 4: Find first element < 4 (which is 3 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 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.

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.

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

How does quick sort divide the problem into subproblems?


Recommended Readings

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

Load More