Shortest Subarray with Sum at Least K
Return the length of the shortest, non-empty, contiguous subarray of A with sum at least K.
If there is no non-empty subarray with sum at least K, return -1.
Example 1:
Input: A = [1], K = 1
Output: 1
Example 2:
Input: A = [1,2], K = 4
Output: -1
Example 3:
Input: A = [2,-1,2], K = 3
Output: 3
Note:
1 <= A.length <= 50000
-10 ^ 5 <= A[i] <= 10 ^ 5
1 <= K <= 10 ^ 9
Difficulty: Hard
这个问题要求出所给数组中,所有数之和至少为K的子数组的最短长度。
建立 long int
类型的数组 sums
,sums[i]
表示前 i 个数的和。那么,就可以由 sums[j] - sums[i]
求出从第 i + 1 个数到第 j 个数的和了。这样只要遍历一次源数组,比对每个数对 ( i , j ) 都相加这个区间内的若干个数要高效。
然后,对每个数对 ( i , j ) ,都检查 sums[j] - sums[i] >= K
是否成立。时间复杂度是 O ( N 2 ) O(N^2) O(N2) 。
class Solution {
public:
int shortestSubarray(vector& A, int K) {
const int size2 = A.size() + 1;
long int sums[size2] = {0};
int min = A.size();
int sum = 0;
for(int i = 1; i < size2; i++){
if(A[i - 1] >= K) return 1;
sums[i] = sums[i - 1] + A[i - 1];
}
for(int i = 0; i < size2; i++){
for(int j = i + 1; j < size2 && j < i + min; j++){
if(sums[j] - sums[i] >= K && min > j - i){
min = j - i;
break;
}
}
}
if(min == A.size() && sums[A.size()] < K) return -1;
return min;
}
};
但是,提交后结果是 Time Limit Exceeded
。有几个源数组很长的输入,这个算法还不够高效。
来看看这个问题中,有哪些能用于优化算法的性质。
可以把 sums
想象成一个折线图,横坐标是 sums
数组的索引,纵坐标是索引对应的元素值。两个相邻的点 sums[i]
和 sums[i + 1]
之间的线段,可看作 A[i]
。
一个符合要求的子数组可由 (begin , end)
来定义,即这个数组之和等于 sums[end] - sums[begin]
,它包括了第 begin + 1
个数到第 end
个数。
① sums[begin]
右边应该是一条上升的线段,也即 A[begin] > 0
或 sums[begin] < sums[begin + 1]
。因为如果 sums[begin] >= sums[begin + 1]
,那么 end - (begin + 1)
是比 end - begin
更小、更好的答案。
② sums[end]
左边也应该是一条上升的线段,也即 A[end - 1] > 0
或 sums[end - 1] < sums[end]
。因为如果 sums[end - 1] >= sums[end]
,那么 (end - 1) - begin
同样是比 end - begin
更小、更好的答案。
综上,我们只要考虑一条上升线段的两个端点就行了。所以,如下面代码,建立 increase
,来记录那些满足 A[i] > 0
的 i
们。
③ 如果选定 begin
后开始寻找对应的 end
,在途中遇到了 sums[i] < sums[begin]
呢?那么对满足这个 begin
的 end
来说,i
是比 begin
更优的 begin
。
所以,如下面代码,在寻找 end
(即 y
)的同时可以更新 minIndex
,也即在 end
之前,最小的 sums[i]
。下一个 begin
应为 minIndex
的下一个数。
class Solution {
public:
int shortestSubarray(vector& A, int K) {
const int size2 = A.size() + 1;
long int sums[size2] = {0};
int min = size2;
int sum = 0;
for(int i = 1; i < size2; i++){
if(A[i - 1] >= K) return 1;
sums[i] = sums[i - 1] + A[i - 1];
}
vector increase;
for(int i = 0; i < A.size(); i++){
if(A[i] > 0) increase.push_back(i);
}
for(int i = 0; i < increase.size(); ){
int minIndex = increase[i];
for(int j = i + 1; j < increase.size(); j++){
int y = increase[j];
if(sums[y] <= sums[minIndex]){
minIndex = y;
i = j;
}
else if(sums[y + 1] >= sums[minIndex] + K && min > y + 1 - minIndex){
min = y + 1 - minIndex;
break;
}
}
i++;
}
if(min == size2) return -1;
return min;
}
};
时间复杂度仍是 O ( N 2 ) O(N^2) O(N2) 。而结果,还是 Time Limit Exceeded
!
怎么办呢?
上面的算法是由 begin
寻找 end
。如果由 end
寻找 begin
呢?
④ 由 ② 可知,对某个 end
来说,如果存在点 sums[i] >= sums[end]
且 i < end
,那么这个 i
不会是这个 end
及后来的 end
们的最佳 begin
。所以,我们可以把这个点 i
去掉,不计算 sums[end] - sums[i]
。
⑤ 第 ④ 个性质中,去掉 i
的操作可以产生一个上升的点序列。对这个 end
来说,如果它的最佳 begin
找到了,那么前面的其他小于 sums[begin]
的 sums[i]
也再不用被这个 end
及后面的其他 end
们考虑。
class Solution {
public:
int shortestSubarray(vector& A, int K) {
const int size2 = A.size() + 1;
long int sums[size2] = {0};
int min = size2;
int sum = 0;
for(int i = 1; i < size2; i++){
if(A[i - 1] >= K) return 1;
sums[i] = sums[i - 1] + A[i - 1];
}
deque list;
for(int end = 0; end < size2; end++){
while(!list.empty() && sums[list.back()] >= sums[end]){
list.pop_back();
}
while(!list.empty() && sums[list.front()] + K <= sums[end]){
int distance = end - list.front();
list.pop_front();
if(min > distance) min = distance;
}
list.push_back(end);
}
if(min == size2) return -1;
return min;
}
};
时间复杂度是 O ( N ) O(N) O(N) 。结果:AC!
在【分析过程】中写的【寻找优化】,和它之后的【由end寻找begin】相比,看起来前者好像没什么用。但是记录下自己曲折的思考过程,我想这也是一种总结、一种收获吧。
还有就是,这道题真难啊,最终还是看了 LeetCode 上的 Solution 才 AC,我好菜啊。