1673. Find the Most Competitive Subsequence
Problem Description
In this problem, we are given an array nums
of integers and another integer k
. Our task is to find the most competitive subsequence of the array nums
that has a size of k
. A subsequence here means a sequence that can be derived by deleting any number of elements (including zero) from the array without changing the order of the remaining elements.
A subsequence is considered more competitive if at the first point of difference between two subsequences, the number in the more competitive one is smaller. For example, the subsequence [1,3,4]
is more competitive than [1,3,5]
because, at their first differing position, 4
is smaller than 5
.
To put it another way, we are looking for the smallest lexicographically subsequence of length k
from the given array, where "smallest" refers to the subsequence that would come first if you listed all possible subsequences of the given length in numerical order.
Intuition
The intuition behind approaching this problem lies in maintaining a stack that can help us keep track of potential candidates for the most competitive subsequence. Here's the thought process:
-
We move through the array and for each element, we decide if it should be included in the stack (which represents the current most competitive subsequence we have built up).
-
We apply two checks while considering to push an element onto the stack: a. If the current element is smaller than the top of the stack (last element in our current subsequence), it could lead to a more competitive subsequence. So we might want to remove the larger elements from the top of the stack. b. However, we can only remove elements from the stack if we are sure that we can still complete our subsequence of size
k
. In other words, there must be enough elements left in the array to fill the stack after popping. This is guaranteed whenlen(stk) + n - i > k
wherestk
is the stack,n
is the number of elements innums
, andi
is the current index. -
After considering the above two points, if our stack size is less than
k
, we push the current element onto the stack as a potential candidate for the most competitive sequence. We keep doing this until we have scanned through all elements in the array. -
After the loop finishes, the stack contains the most competitive subsequence of size
k
.
The stack effectively stores the smallest numbers seen so far and pops the larger ones that could be replaced by the following smaller numbers to maintain the competitiveness of the sequence. By the time the scanning of the array is complete, we have the most competitive subsequence.
Learn more about Stack, Greedy and Monotonic Stack patterns.
Solution Approach
The solution uses a stack as a data structure to implement a greedy approach. Here's a step-by-step breakdown of the algorithm:
-
Initialize an empty list called
stk
which will be treated like a stack. This will be used to store the elements of the most competitive subsequence. -
Determine the length of the input array
nums
and store it in a variablen
. -
Loop through each element
v
and its indexi
in the array usingenumerate(nums)
. -
Inside the loop, there is a
while
loop with a compound condition that is used to decide whether the top element should be popped from the stack:- The stack should not be empty (there's an element to potentially pop).
- The current element
v
is less than the last element in the stack (stk[-1]
). This check is important because a smaller element makes a subsequence more competitive. - There are still enough elements left in
nums
(afteri
th index) to fill the stack to a length ofk
(the conditionlen(stk) + n - i > k
ensures this).
-
If the above conditions are met, the top element of the stack is popped because keeping it would make our subsequence less competitive. We continue popping until the conditions fail, meaning we either exhaust the stack or the current element
v
is not smaller than the last instk
or there aren't enough elements remaining to reach lengthk
. -
After the
while
loop, a check ensures that we only add elements tostk
if its length is less thank
. Otherwise, our subsequence would exceed the desired length. -
We append the current element
v
to the stack if the length of the stack is less thank
. -
After iterating over all elements in
nums
, the liststk
will contain exactlyk
elements, which forms the most competitive sequence possible, and thus is returned.
During each iteration, the algorithm greedily tries to make the subsequence as small as possible by comparing the current number with the ones already in stk
. This use of a stack allows the algorithm to keep the subsequence always in a competitive state by ensuring that each new added number would not make it any worse compared to other potential subsequences.
The main algorithmic patterns used here are:
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with a small example. Suppose we have the following array nums
and integer k
:
nums = [3, 5, 2, 6] k = 2
We want to find the most competitive subsequence of size k
from nums
.
Following the solution approach:
-
Initialize an empty stack
stk
. -
Determine the length of
nums
, which isn = 4
. -
Begin iterating through
nums
:-
At index
i = 0
,v = 3
, there is nothing to pop becausestk
is empty. We add3
tostk
. -
At index
i = 1
,v = 5
, there is nothing to pop because5
is not less than the last element instk
, which is3
. Sincelen(stk) < k
, we add5
tostk
. -
At index
i = 2
,v = 2
,v
is less than the last element instk
(which is5
) andlen(stk) + (n - i) > k
(2
elements instk
+2
elements left innums
>2
). So we pop5
fromstk
and add2
. -
At index
i = 3
,v = 6
, there is nothing to pop since6
is greater than the last element instk
(which is2
). However,len(stk)
is alreadyk
, so we don’t add6
tostk
.
-
-
After the loop finishes,
stk = [2, 3]
which forms the most competitive subsequence because it is lexicographically the smallest subsequence of lengthk
that we can form fromnums
.
The resulting most competitive subsequence is [2, 3]
. The stack has helped us maintain the potential candidates for the subsequence efficiently, ensuring at each step that any larger numbers that could be replaced by smaller ones and do not contribute to the competitiveness are removed.
Solution Implementation
1class Solution:
2 def mostCompetitive(self, nums: List[int], k: int) -> List[int]:
3 # Initialize an empty list to use as a stack
4 stack = []
5 # Get the total number of elements in nums
6 num_len = len(nums)
7
8 # Iterate over the indices and values of nums
9 for i, value in enumerate(nums):
10 # While stack is not empty and the last element in stack is greater than the current value
11 # and there are enough remaining elements to replace and still build a sequence of length k
12 while stack and stack[-1] > value and len(stack) + num_len - i > k:
13 # Pop the last element from the stack
14 stack.pop()
15 # If the stack contains fewer than k elements, append the current value
16 if len(stack) < k:
17 stack.append(value)
18
19 # Return the stack which now contains the most competitive subsequence of length k
20 return stack
21
1class Solution {
2
3 public int[] mostCompetitive(int[] nums, int k) {
4 // Use a deque as a stack to maintain the most competitive sequence.
5 Deque<Integer> stack = new ArrayDeque<>();
6 int n = nums.length;
7
8 // Iterate over the array elements.
9 for (int i = 0; i < nums.length; ++i) {
10 // While the stack is not empty, and the current element is smaller than the top element of the stack,
11 // and there are enough elements remaining to form a sequence of length k
12 // we pop the stack since the current element gives a more competitive sequence.
13 while (!stack.isEmpty() && stack.peek() > nums[i] && stack.size() + n - i > k) {
14 stack.pop();
15 }
16
17 // If the stack size is less than k, we can push the current element onto the stack.
18 if (stack.size() < k) {
19 stack.push(nums[i]);
20 }
21 }
22
23 // Create an array to store the most competitive sequence.
24 int[] answer = new int[stack.size()];
25 // Pop elements from the stack to fill the answer array in reverse order
26 // since the elements in deque are in reverse order of the required sequence.
27 for (int i = answer.length - 1; i >= 0; --i) {
28 answer[i] = stack.pop();
29 }
30 // Return the array containing the most competitive sequence
31 return answer;
32 }
33}
34
1#include <vector>
2
3class Solution {
4public:
5 // Function to find the most competitive subsequence of `nums` with size `k`
6 std::vector<int> mostCompetitive(std::vector<int>& nums, int k) {
7 std::vector<int> stack; // Stack to store the most competitive subsequence
8 int numsSize = nums.size(); // Store the size of the input vector
9
10 // Iterate over all the elements in the input vector
11 for (int i = 0; i < numsSize; ++i) {
12 // While the stack is not empty, the last element in the stack is greater than
13 // the current number, and the potential size of the competitive subsequence
14 // is still greater than `k`, pop the last element from the stack
15 while (!stack.empty() && stack.back() > nums[i] && stack.size() + numsSize - i > k) {
16 stack.pop_back();
17 }
18 // If the size of the stack is less than `k`, we can add the current number
19 // to the subsequence
20 if (stack.size() < k) {
21 stack.push_back(nums[i]);
22 }
23 }
24 // Return the most competitive subsequence
25 return stack;
26 }
27};
28
1function mostCompetitive(nums: Array<number>, k: number): Array<number> {
2 let stack: Array<number> = []; // Stack to store the most competitive subsequence
3 let numsSize = nums.length; // Store the length of the input array
4
5 // Iterate over all the elements in the input array
6 for (let i = 0; i < numsSize; ++i) {
7 // While the stack is not empty and the last element in the stack is greater than
8 // the current element, and there are enough remaining elements to reach a
9 // competitive subsequence of size `k`, pop the last element from the stack
10 while (
11 stack.length > 0 &&
12 stack[stack.length - 1] > nums[i] &&
13 stack.length + numsSize - i > k
14 ) {
15 stack.pop();
16 }
17
18 // If the current size of the stack is less than `k`, we can add the current element
19 // to the subsequence
20 if (stack.length < k) {
21 stack.push(nums[i]);
22 }
23 }
24
25 // Return the most competitive subsequence
26 return stack;
27}
28
29// Example usage:
30// let result = mostCompetitive([3, 5, 2, 6], 2);
31// result would be [2, 6]
32
Time and Space Complexity
Time Complexity
The time complexity of the code is O(n)
, where n
is the length of the input list nums
. Even though there is a nested loop caused by the while
loop within the for
loop, each element is pushed to and popped from the stack at most once. This guarantees that each operation is bounded by 2n
(each element pushed and popped at most once), which simplifies to O(n)
.
Space Complexity
The space complexity is O(k)
since we are using a stack to keep track of the most competitive subsequence, which, according to the problem, can have a maximum length of k
. No other data structures are used that grow with the size of the input.
Learn more about how to find time and space complexity quickly using problem constraints.
Which of the following uses divide and conquer strategy?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!