LeetCode 862. 和至少为 K 的最短子数组
最近在刷LeetCode,随便写点备忘巩固一下。
题目
返回 A 的最短的非空连续子数组的长度,该子数组的和至少为 K 。
如果没有和至少为 K 的非空子数组,返回 -1 。
示例 1:
输入:A = [1], K = 1
输出:1
示例 2:
输入:A = [1,2], K = 4
输出:-1
示例 3:
输入:A = [2,-1,2], K = 3
输出:3
提示:
1.1 <= A.length <= 50000
2.-10 ^ 5 <= A[i] <= 10 ^ 5
3.1 <= K <= 10 ^ 9
解法1
直接想到一个题,求最大子数组和,在算法优化后可以在O(n)时间内完成.这题稍微有些变化,但是有一点点像.假设对每一个数组中的,计算以它开始子数组和,一旦满足后就与当前最短对比,后续就不用算了.
class Solution {
public int shortestSubarray(int[] A, int K) {
int size = A.length;
int ans = size + 1;
for (int i = 0; i < size; ++i) {
if (A[i] >= K)
return 1;
int sum = A[i], j = 1;
while (sum > 0 && j < ans && j < size - i) {
sum += A[i+j];
++j;
if (sum >= K)
ans = j;
}
}
if (ans == size + 1)
return -1;
else
return ans;
}
}
嗯,结果显然是超时了。时间复杂度O(n²)
解法2
试试动态规划吧
class Solution {
public:
int shortestSubarray(vector<int>& A, int K) {
int size=A.size();
int temp[size];
for(int i=0;i<size;++i) {
if(A[i]>=K)
return 1;
temp[i]=A[i];
}
for(int i=2;i<=size;++i) {
for(int j=0;j<=size-i;++j) {
if(temp[j]>0) {
temp[j]+=A[i+j-1];
if (temp[j]>=K)
return i;
}
}
}
return -1;
}
};
建立一个数组temp
,用于存放长度为i
时以各元素为起点的子数组和。
emmmmm,这时间复杂度不还是O(n²)嘛!
果不其然,继续超时
解法3
想不出来,我认怂了,在社区找了个别人的代码
class Solution {
public int shortestSubarray(int[] A, int K) {
int result = 0x7fffffff;
int[] distance = new int[A.length + 1];
distance[0] = 0;
for(int i = 1; i <= A.length; i++) {
distance[i] = A[i - 1] + distance[i-1];
}
//维护单调栈 从右边开始入栈 栈底就是当前元素往右走最大的元素
//如果从该元素出发有最解 则解的右端必定位于当前元素与当前元素往右的最大元素之间
//且解的右端必定在栈内(因为栈单调往栈底递减 所有比当前元素大的元素均在栈中)
//再对该区间进行二分法求最靠近左端的元素
//进一步优化可以使用单调队列
//将过于遥远(大于当前最优解距离)的栈底最大元素变为队列头部出队
LinkedList<Integer> queue = new LinkedList<>();
for(int i = A.length; i >= 0; i--) {
//去除过于遥远的队头元素
while(!queue.isEmpty() && queue.peek()-i >= result) queue.removeFirst();
//当新入队元素比队尾元素大时 队尾元素出队
while(!queue.isEmpty() && distance[queue.peekLast()] <= distance[i]) queue.removeLast();
//当前元素入队
queue.add(i);
//开始二分搜索
if(distance[queue.peek()] >= distance[i] + K) { //先直接判断最大元素是否满足条件
//二分搜索最短距离
int currentMinLength = binarySearch(queue, i, distance, K);
//判断是否当前最短距离
result = result < currentMinLength? result : currentMinLength;
}
}
return result == 0x7fffffff ? -1 : result;
}
public int binarySearch(LinkedList<Integer> queue, int i, int[] distance, int K) {
//二分搜索初始上下边界
int downer = 0, upper = queue.size()-1, mid = 0;
//最终结果下标
int j = 0x7fffffff;
while(upper >= downer) {
mid = (upper + downer) >> 1;
if(distance[queue.get(mid)] >= distance[i] + K) {
j = queue.get(mid);
downer = mid + 1;
}
else upper = mid - 1;
}
return j == 0x7fffffff ? 0x7fffffff : (j - i);
}
}
运用单调队列实现。所谓单调队列,就是一端进,两端出的队列了。每一个新元素入队前,首先检查队首元素是否过于陈旧,若是就移除 ;随后检查队尾元素是否比当前元素值更大,若不是则移除。
假设某时刻队列中从队首到队尾有三个元素a
、b
、c
,此时来了个d
∈(b,c),假设a过旧,则d入队后队列将会变成b
、d
。
回归本题,解法2的动态规划法实际上是按照子数组长度从低到高生成,看似一旦出现解立刻就可以得到答案,后续更长的都不需要算了,实际时间复杂度仍然是O(n²)级。解法3的巧妙之处在于,它逐步计算每个子数组的最大结果,而不是一次计算整个数组(这么一看之前的解法2根本算不上动态规划了)。
考虑Si(j)为以i
为起点的,长度为j
的子数组和。那么有
Si(j)=S0(i+j)-S0(i)
由此可以直接计算任何位置任何长度的子数组和。
distance
数组实际就是S0。所以只要得知distance[队首]
-distance[当前位置]
≥K
,就说明当前位置开始的子数组有满足条件的存在。那么假设某时刻单调队列里队首至队尾有三个元素q1
、q2
、q3
则有distance[q1]
>distance[q2]
>distance[q3]
和q1
>q2
>q3
。故可以用二分查找加速搜索过程。
算法整体时间复杂度为O(nlog(n)),提交通过(废话)。