{x}
blog image

Find the Most Competitive Subsequence

Given an integer array nums and a positive integer k, return the most competitive subsequence of nums of size k.

An array's subsequence is a resulting sequence obtained by erasing some (possibly zero) elements from the array.

We define that a subsequence a is more competitive than a subsequence b (of the same length) if in the first position where a and b differ, subsequence a has a number less than the corresponding number in b. For example, [1,3,4] is more competitive than [1,3,5] because the first position they differ is at the final number, and 4 is less than 5.

 

Example 1:

Input: nums = [3,5,2,6], k = 2
Output: [2,6]
Explanation: Among the set of every possible subsequence: {[3,5], [3,2], [3,6], [5,2], [5,6], [2,6]}, [2,6] is the most competitive.

Example 2:

Input: nums = [2,4,3,3,5,4,9,6], k = 4
Output: [2,3,3,4]

 

Constraints:

  • 1 <= nums.length <= 105
  • 0 <= nums[i] <= 109
  • 1 <= k <= nums.length

Solution Explanation: Finding the Most Competitive Subsequence

This problem asks us to find the lexicographically smallest subsequence of length k from the input array nums. The solution uses a greedy approach with a stack to efficiently achieve this.

Algorithm:

The core idea is to maintain a stack (stk) that stores the currently most competitive subsequence seen so far. We iterate through the input array nums. For each number nums[i]:

  1. Check for Improvement: While the stack is not empty, the top element of the stack is greater than the current number (nums[i]), and removing the top element from the stack will still allow us to form a subsequence of length k (i.e., len(stk) + (n - i) > k, where n is the length of nums), we pop the top element. This step ensures that we are always building a subsequence with the smallest possible numbers at each position.

  2. Push onto Stack: If the stack has fewer than k elements, we push the current number (nums[i]) onto the stack. This ensures that we maintain a subsequence of length k.

  3. Final Result: After iterating through the entire array, the stack contains the most competitive subsequence. We return the stack's content.

Time Complexity: O(n), where n is the length of nums. In the worst-case scenario, we iterate through the array once, and each element is pushed and potentially popped from the stack at most once. The operations on the stack (push and pop) take constant time O(1).

Space Complexity: O(k). The maximum size of the stack is k, as we are only interested in subsequences of length k.

Code Examples:

The following code demonstrates the solution in various programming languages. The logic remains consistent across all implementations.

Python:

class Solution:
    def mostCompetitive(self, nums: List[int], k: int) -> List[int]:
        stk = []
        n = len(nums)
        for i, v in enumerate(nums):
            while stk and stk[-1] > v and len(stk) + n - i > k:
                stk.pop()
            if len(stk) < k:
                stk.append(v)
        return stk

Java:

import java.util.ArrayDeque;
import java.util.Deque;
 
class Solution {
    public int[] mostCompetitive(int[] nums, int k) {
        Deque<Integer> stk = new ArrayDeque<>();
        int n = nums.length;
        for (int i = 0; i < n; ++i) {
            while (!stk.isEmpty() && stk.peek() > nums[i] && stk.size() + n - i > k) {
                stk.pop();
            }
            if (stk.size() < k) {
                stk.push(nums[i]);
            }
        }
        int[] ans = new int[stk.size()];
        for (int i = ans.length - 1; i >= 0; --i) {
            ans[i] = stk.pop();
        }
        return ans;
    }
}

C++:

#include <vector>
#include <stack>
 
class Solution {
public:
    vector<int> mostCompetitive(vector<int>& nums, int k) {
        stack<int> stk;
        int n = nums.size();
        for (int i = 0; i < n; ++i) {
            while (!stk.empty() && stk.top() > nums[i] && stk.size() + n - i > k) {
                stk.pop();
            }
            if (stk.size() < k) {
                stk.push(nums[i]);
            }
        }
        vector<int> ans;
        while (!stk.empty()) {
            ans.push_back(stk.top());
            stk.pop();
        }
        reverse(ans.begin(), ans.end()); // Reverse to maintain original order
        return ans;
    }
};
 

Go:

func mostCompetitive(nums []int, k int) []int {
	stk := []int{}
	n := len(nums)
	for i, v := range nums {
		for len(stk) > 0 && stk[len(stk)-1] > v && len(stk)+n-i > k {
			stk = stk[:len(stk)-1]
		}
		if len(stk) < k {
			stk = append(stk, v)
		}
	}
	return stk
}

These examples all implement the same core algorithm with minor syntactic differences due to language features. They all achieve linear time complexity and use space proportional to k.