Question
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
Algorithm-1
(单调栈) O(n)
首先将题目转化成更直观的问题:寻找长度为k的字典序最小的子序列。字典序最小意味着越小的数字应该排在高位,对于当前数字nums[i],假设现在已经找到了一个子序列长度为k,现在我们想让nums[i]排在这个子序列的第pos个位置,那么意味着子序列里从pos + 1到k - 1的数字应该被排除掉,这也就意味着在原数组里位于nums[i]之前位置的数字被排除掉并且不会被使用,所以想到采用单调栈求解。
但是注意,单调栈里只是近似从栈底到栈顶单增,因为考虑如果对于nums[i]及其后面的数字,和之前已经选出来的数字恰好为k个,此时则不一定是单增。那么判别的一种办法就是用dropNum记录有多少个数字被丢弃掉了,如果dropNum到了n - k,那么从当前数字开始及以后的都直接入栈即可。如果dropNum还未到n - k,则延续单调栈的做法。
于是问题集中在如何处理dropNum,首先当延续单调栈的做法时候,从栈内弹出的数字肯定是要被丢弃的,另外就是栈内的元素已经满掉,并且当前数字不满足进栈条件,也会被丢弃。
时间复杂度O(n) ,空间复杂度O(n) 。
Code-1
C++
class Solution {
public:
vector<int> mostCompetitive(vector<int>& nums, int k) {
std::ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int n = nums.size();
if (n == k) return nums;
int dropNum = 0;
stack<int> s;
for (int i = 0; i < n; ++i) {
if ((int)s.size() >= k && nums[i] >= s.top()) {
++dropNum;
continue;
}
if (dropNum >= n - k) s.emplace(nums[i]);
else {
if (s.empty()) s.emplace(nums[i]);
else {
while (!s.empty() && dropNum < n - k && nums[i] < s.top()) {
s.pop();
++dropNum;
}
s.emplace(nums[i]);
}
}
}
vector<int> res(k);
int pos = k - 1;
while (!s.empty()) {
res[pos--] = s.top(); s.pop();
}
return res;
}
};
Algorithm-2
给定一个数组A,求其字典序最小的长k的子序列。 思路是单调栈。求长k的子序列相当于在A里删掉lA−k个数字。我们用一个变量r标记还可以删多少个数字,一开始赋值为lA−k。接着开一个从栈底到栈顶严格单调上升的栈,如果栈非空并且栈顶大于新来的数,并且还能删数字(指r > 0成立),则出栈,并将r减1,直到栈空或者栈顶小于等于新来的数或者r = 0(这时没法删了),接着将新来的数入栈(如果栈满,说明新来的数无法入栈,则多删一个数,将r减去1)。最后栈底到栈顶的数组即为所求。算法正确性证明,只需要注意每次可以删的时候(这里的删主要指的是pop栈顶),删总是比不删好。代码如下:
Code-2
C++
public class Solution {
public int[] mostCompetitive(int[] nums, int k) {
int[] res = new int[k];
int idx = 0, rem = nums.length - k;
for (int i = 0; i < nums.length; i++) {
// 如果栈非空并且栈顶大于新来的数,并且还能删数字,则出栈
while (idx > 0 && res[idx - 1] > nums[i] && rem > 0) {
idx--;
rem--;
}
if (idx < res.length ) {
res[idx++] = nums[i];
} else {
rem--;
}
}
return res;
}
}